From 85d423b5c96b3f70803fb74b919ac4b941a49f72 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 17 May 2026 01:11:13 +0200 Subject: [PATCH 01/94] =?UTF-8?q?init(openai-agents):=20Add=20initial=20Py?= =?UTF-8?q?thon=20SDK=20integration=20for=20AG-UI=20=C3=97=20OpenAI=20Agen?= =?UTF-8?q?ts=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Configure `pyproject.toml` with project metadata and dependencies. - Include required `.python-version` file. - Add `README.md` with installation instructions. - Generate `uv.lock` file with dependency resolution. --- integrations/openai-agents/python/.gitignore | 221 +++ integrations/openai-agents/python/README.md | 12 + .../openai-agents/python/pyproject.toml | 21 + integrations/openai-agents/python/uv.lock | 1326 +++++++++++++++++ 4 files changed, 1580 insertions(+) create mode 100644 integrations/openai-agents/python/.gitignore create mode 100644 integrations/openai-agents/python/README.md create mode 100644 integrations/openai-agents/python/pyproject.toml create mode 100644 integrations/openai-agents/python/uv.lock diff --git a/integrations/openai-agents/python/.gitignore b/integrations/openai-agents/python/.gitignore new file mode 100644 index 0000000000..30b72f3bd5 --- /dev/null +++ b/integrations/openai-agents/python/.gitignore @@ -0,0 +1,221 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +*.lcov +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi/* +!.pixi/config.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule* +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. + .idea/ + .junie/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml \ No newline at end of file diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md new file mode 100644 index 0000000000..18c8c6845a --- /dev/null +++ b/integrations/openai-agents/python/README.md @@ -0,0 +1,12 @@ +# AG-UI × OpenAI Agents SDK + +Integrates [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) +with [AG-UI Protocol](https://github.com/ag-ui-protocol/ag-ui). Build your +agent with the OpenAI SDK as usual, then stream its execution as AG-UI events. + +## Install + +```bash +pip install ag-ui-openai-agent-sdk +uv sync +``` \ No newline at end of file diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml new file mode 100644 index 0000000000..7697d56e79 --- /dev/null +++ b/integrations/openai-agents/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "ag-ui-openai-agent-sdk" +version = "0.1.0" +description = "AG-UI integration for OpenAI Agent SDK" +readme = "README.md" +authors = [ + { name = "Abdelrahman Abozied" }, +] +requires-python = ">=3.9" +dependencies = [ + "ag-ui-protocol>=0.1.18", + "openai-agents>=0.8.4", + "pydantic>=2.13.4", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/ag_ui_openai_agents"] diff --git a/integrations/openai-agents/python/uv.lock b/integrations/openai-agents/python/uv.lock new file mode 100644 index 0000000000..11840d2f62 --- /dev/null +++ b/integrations/openai-agents/python/uv.lock @@ -0,0 +1,1326 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "ag-ui-openai-agent-sdk" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "ag-ui-protocol" }, + { name = "openai-agents", version = "0.8.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "openai-agents", version = "0.17.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "ag-ui-protocol", specifier = ">=0.1.18" }, + { name = "openai-agents", specifier = ">=0.8.4" }, + { name = "pydantic", specifier = ">=2.13.4" }, +] + +[[package]] +name = "ag-ui-protocol" +version = "0.1.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/d7/5711eada86da9bd7684e58645653a1693ef20b66cc3efbb1deeafef80f8d/ag_ui_protocol-0.1.18.tar.gz", hash = "sha256:b37c672c3fd6bac12b316c39f45ad9db9f137bbb885489c79f268507029a22ff", size = 9937, upload-time = "2026-04-21T20:44:59.151Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/74/913c9b8fc566c6da650aecbddf25a5d8186b54138df265eb9eb546f56141/ag_ui_protocol-0.1.18-py3-none-any.whl", hash = "sha256:d151c0f0a34160647f1571163f7185746f4326b15a56d1560de5082a7a0e7a12", size = 12607, upload-time = "2026-04-21T20:45:00.097Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, + { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/2e/a9959997739c403378d0a4a3a1c4ed80b60aeace216c4d37b303a9fc60a4/jiter-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:02f36a5c700f105ac04a6556fe664a59037a2c200db3b7e88784fac2ddf02531", size = 316927, upload-time = "2026-04-10T14:25:40.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/72/b6de8a531e0adbadd839bec301165feb1fccf00e9ff55073ba2dd20f0043/jiter-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41eab6c09ceffb6f0fe25e214b3068146edb1eda3649ca2aee2a061029c7ba2e", size = 321181, upload-time = "2026-04-10T14:25:42.621Z" }, + { url = "https://files.pythonhosted.org/packages/db/d8/2040b9efa13c917f855c40890ae4119fe02c25b7c7677d5b4fa820a851fc/jiter-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf4d4c109641f9cfaf4a7b6aebd51654e405cd00fa9ebbf87163b8b97b325aa", size = 347387, upload-time = "2026-04-10T14:25:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/655c0ad5ce6a8e90f9068c175b8a236877d753e460762b3183c136db1c5b/jiter-0.14.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80c7b41a628e6be2213ad0ece763c5f88aa5ee003fa394d58acaaee1f4b8342", size = 373083, upload-time = "2026-04-10T14:25:45.55Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/549c40fa068f08710b7570869c306a051eb67a29758bd64f4114f730554c/jiter-0.14.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb3dbf7cc0d4dbe73cce307ebe7eefa7f73a7d3d854dd119ea0c243f03e40927", size = 463639, upload-time = "2026-04-10T14:25:47.452Z" }, + { url = "https://files.pythonhosted.org/packages/25/2f/97a32a05fed14ed58a18e181fdfb619e05163f3726b54ee6080ec0539c09/jiter-0.14.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7054adcdeb06b46efd17b5734f75817a44a2d06d3748e36c3a023a1bb52af9ec", size = 380735, upload-time = "2026-04-10T14:25:49.305Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3b/4347e1d6c2a973d653bbb7a2d671a2d2426e54b52ba735b8ff0d0a29b75c/jiter-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d597cd1bf6790376f3fffc7c708766e57301d99a19314824ea0ccc9c3c70e1e2", size = 358632, upload-time = "2026-04-10T14:25:50.931Z" }, + { url = "https://files.pythonhosted.org/packages/ef/24/ca452fbf2ea33548ed30ce68a39a50442d3f7c9bf0704a7af958a930c057/jiter-0.14.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:df63a14878da754427926281626fd3ee249424a186e25a274e78176d42945264", size = 359969, upload-time = "2026-04-10T14:25:52.381Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a3/94470a0d199287caabeb4da2bb2ae5f6d17f3cf05dfc975d7cb064d58e0f/jiter-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ea73187627bcc5810e085df715e8a99da8bdfd96a7eb36b4b4df700ba6d4c9c", size = 397529, upload-time = "2026-04-10T14:25:53.801Z" }, + { url = "https://files.pythonhosted.org/packages/cf/71/6768edc09d7c45c39f093feb3de105fa718a3e982b5208b8a2ed6382b44b/jiter-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9f541eaf7bb8382367a1a23d6fc3d6aad57f8dd8c18c3c17f838bee20f217220", size = 522342, upload-time = "2026-04-10T14:25:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6b/5c2e17559a0f4e96e934479f7137df46c939e983fa05244e674815befb73/jiter-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:107465250de4fce00fdb47166bcd51df8e634e049541174fe3c71848e44f52ce", size = 556784, upload-time = "2026-04-10T14:25:56.927Z" }, + { url = "https://files.pythonhosted.org/packages/b1/83/c25f3556a60fc74d11199100f1b6cc0c006b815c8494dea8ca16fe398732/jiter-0.14.0-cp310-cp310-win32.whl", hash = "sha256:ffb2a08a406465bb076b7cc1df41d833106d3cf7905076cc73f0cb90078c7d10", size = 208439, upload-time = "2026-04-10T14:25:58.796Z" }, + { url = "https://files.pythonhosted.org/packages/2e/99/781a1b413f0989b7f2ea203b094b331685f1a35e52e0a45e5d000ecaab27/jiter-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb8b682d10cb0cce7ff4c1af7244af7022c9b01ae16d46c357bdd0df13afb25d", size = 204558, upload-time = "2026-04-10T14:26:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, + { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, + { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, + { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, + { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, + { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/704f5027508138f22cda277779bc6b9cace77af1a87ef93c73daf61b5b12/jiter-0.14.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:85581c4c3e4060fe3424cdfd7f3aa610f2dc5e9dde8b6863358eb68560018472", size = 319926, upload-time = "2026-04-10T14:28:04.687Z" }, + { url = "https://files.pythonhosted.org/packages/54/f6/ee3ddf36a0989959760417fbe662e4f7bc6451547a54262ef17c2882875d/jiter-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c6279c63849444a4fe9b9abf82e5df0fc7d13dea07f53f084b362485bd1f2bbe", size = 315333, upload-time = "2026-04-10T14:28:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/63/c1/ba033578d36c99455f5fbd8bbce891c29e2348dfa25811875a636975d82f/jiter-0.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59940ef6ac9f8b34c800838416f105f0503485fa8d71cae99f71d44a7285b01e", size = 349996, upload-time = "2026-04-10T14:28:07.807Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fd/b6c8b38d1c53bd115abdd3143038f6f29fa4eaad915205ca959e5a16cc08/jiter-0.14.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55bee2b6a2657434984d9144c20cf27ba3b6acd495539539953e447778515efd", size = 373255, upload-time = "2026-04-10T14:28:09.597Z" }, + { url = "https://files.pythonhosted.org/packages/e4/48/153044aee4ebc7e4c92be697dd96da8e8eb7f920e1e90e4dda0c5aa7e400/jiter-0.14.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d45fc7ea86a46bd9b5bceb9e8d43e5d10a392378713fb32cf1ce851b4b0d1f8", size = 466311, upload-time = "2026-04-10T14:28:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/2d7f2d29543f8d2960396cc44068228ef9badaac35998b255277b439d3b4/jiter-0.14.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:758d19dae7ea4c4da3cbc463dc323d1660e7353144ef17509ff43beab6da5a47", size = 382612, upload-time = "2026-04-10T14:28:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8c/0522d2dac614f141111d80fb9ca1363251dcef796b21523d4a9f78684f88/jiter-0.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32959d7285d1d0deb5a8c913349e476ad9271b384f3e54cca1931c4075f54c6e", size = 361841, upload-time = "2026-04-10T14:28:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/e8/48/53b5627bf5027fd9d49a35c5921e3c220140b51ef68814bfc93750c562b1/jiter-0.14.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:78a4c677fe5689e0e129b39f5affe9210a500b6620ebb0386ebccf5922bee9a6", size = 362913, upload-time = "2026-04-10T14:28:16.583Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b5/5dde259eec978b54be123128311ee0e9eb4cc095019d06ad45401913567f/jiter-0.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ae66782ecffb1a266e1a07f5abbfc3832afdd260fc9b478982c3f8e01eba5fa", size = 399862, upload-time = "2026-04-10T14:28:18.779Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0a/ef4bce553be8d9e2bbbdbdd3c0337c54773c8a9057c969242afa770d24d3/jiter-0.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:155dab67beac8d66cec9479c93ee2cbe7bfbc67509e5c2860e02ec2d9b0ecca1", size = 524733, upload-time = "2026-04-10T14:28:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/0b/e2416d657d1a9a2503f3edcb493cec5b770b8cb417ccab7ed013b56242c8/jiter-0.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f16b76d7d6aadbbaf7f79a76ff3a51dae14b7ebaaf9c1ba61607784ef51c537c", size = 558776, upload-time = "2026-04-10T14:28:22.85Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/aaed54dfa1c0375df395b2fc6cf7cfb86d480c1e38b821006207774c04de/jiter-0.14.0-cp39-cp39-win32.whl", hash = "sha256:0fbad7aa06f87e8215d660fc6f05a9b07b58751a29967bbd9c81ff22d21dbe8c", size = 211145, upload-time = "2026-04-10T14:28:24.654Z" }, + { url = "https://files.pythonhosted.org/packages/0b/19/1a7be527189cdd61694cd98b83fd382e614e52053bc992728d1cda55022e/jiter-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:e1765c3ef3ea31fe6e282376a16def1a96f5f11a0235055696c18d9d23ff30cb", size = 206779, upload-time = "2026-04-10T14:28:26.31Z" }, + { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, + { name = "referencing", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "mcp" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-settings", marker = "python_full_version >= '3.10'" }, + { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", marker = "python_full_version >= '3.10'" }, + { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, + { name = "starlette", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, +] + +[[package]] +name = "openai" +version = "2.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/50/5901f01ef14e6c27788beb91e54fef5d6204fb5fb9e97402fc8a14de2e32/openai-2.37.0.tar.gz", hash = "sha256:f4bc562cc5f3a43d40d678105572d9d44765f6e0f50c125f63055419b72f4bd9", size = 754706, upload-time = "2026-05-15T22:30:35.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/4c/bce61680d0699a78a405fd9a67989b175ba020590428831aab2ab1d2be7c/openai-2.37.0-py3-none-any.whl", hash = "sha256:814633888b8f3b1ffd6615697c6e4ef93632d08b7c2e28c8c5ef3556e5a10107", size = 1303238, upload-time = "2026-05-15T22:30:32.767Z" }, +] + +[[package]] +name = "openai-agents" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "griffe", marker = "python_full_version < '3.10'" }, + { name = "openai", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/e0/9fa9eac9baf2816bc63cee28967d35a7ed9dc2f25e9fd2004f48ed6c8820/openai_agents-0.8.4.tar.gz", hash = "sha256:5d4c4861aedd56a82b15c6ddf6c53031a39859a222f08bbd5645d5967efa05e8", size = 2389744, upload-time = "2026-02-11T19:14:30.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/dc/10df015aebb0797a8367aab65200ac4f5221df20bbae76930f5b6ac8e001/openai_agents-0.8.4-py3-none-any.whl", hash = "sha256:2383c6e8e59ed4146b89d1b6f53e34e55caf94bc14ae3fd704e7aad5021f4ff1", size = 380662, upload-time = "2026-02-11T19:14:28.864Z" }, +] + +[[package]] +name = "openai-agents" +version = "0.17.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "griffelib", marker = "python_full_version >= '3.10'" }, + { name = "mcp", marker = "python_full_version >= '3.10'" }, + { name = "openai", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "types-requests", version = "2.33.0.20260513", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "websockets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/cd/14acaf94c6a438cfe72c5ea043bfbc77d7fbb9514ab7796d82f2180d1518/openai_agents-0.17.2.tar.gz", hash = "sha256:5e11414bdd8c20c8e9192d21f78265d30e65992f4537f940309eca9255804449", size = 5403689, upload-time = "2026-05-12T03:14:57.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/71/5ba9afa4b6a0d250bfdbdd1cf9a5255ae131a7f82915c634ac50c0633c3a/openai_agents-0.17.2-py3-none-any.whl", hash = "sha256:1b3560c1690bcee635a487f77ebfb8b4fb2dd52a653e045a86e51974ab87faf3", size = 838225, upload-time = "2026-05-12T03:14:55.149Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/5d/00/13a0c039569d1e583779ee1b8d7df6bfe275a0db83fcae14f01d6856c16e/pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae", size = 2115337, upload-time = "2026-05-06T13:38:37.741Z" }, + { url = "https://files.pythonhosted.org/packages/41/60/e70fa1ee03e243bdfd4b1fddf1e1f2a8fba681df3034b51b9376c0fb5bf5/pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201", size = 1957976, upload-time = "2026-05-06T13:37:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/11/9a/78fb5f2ea849f767ea802de8b4e8f5a0c4a48ddbe4bc66bd19ac2f55a01c/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0", size = 1979390, upload-time = "2026-05-06T13:36:52.419Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7d/3acfdcd000bad9735de0430a88355948469781f62cb841fd63e8a307e80e/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15", size = 2043263, upload-time = "2026-05-06T13:39:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/1325e5a8d7f9697416481c7f7c1c304738d6b961a7fd1ea0f054ce0f14fb/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76", size = 2225708, upload-time = "2026-05-06T13:40:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9ec8c38f33b26db0b612cb7fd165bb0a370773710432a2a74fa31287b430/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49", size = 2288494, upload-time = "2026-05-06T13:38:00.091Z" }, + { url = "https://files.pythonhosted.org/packages/65/05/497446a9586d1b2d24ee25ebe208beb15388f1875d783e1e014055d150ac/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928", size = 2095629, upload-time = "2026-05-06T13:38:23.632Z" }, + { url = "https://files.pythonhosted.org/packages/93/d9/cd5fa98f9d94f9294c15459396c8a2383c164469e679ac178d6d42cfee6b/pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066", size = 2119309, upload-time = "2026-05-06T13:39:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/20/1b/64cec655451ddbf3976df5dc9706b240df4fdaebdeebeadd4f59a8dab926/pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6", size = 2170216, upload-time = "2026-05-06T13:39:14.561Z" }, + { url = "https://files.pythonhosted.org/packages/2a/21/fe9f039138c9ea3be10ccdb6ec490acb54dcbef5a5e96dbdf1411f82b929/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9", size = 2186726, upload-time = "2026-05-06T13:37:51.597Z" }, + { url = "https://files.pythonhosted.org/packages/44/cb/19ca0da64821d1aefcef65f253aa9ecbdd0dde360f607d0f9b3d95db2b4e/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29", size = 2320400, upload-time = "2026-05-06T13:39:36.29Z" }, + { url = "https://files.pythonhosted.org/packages/cd/14/fe3fbf6e845bf2080dc2f282d75085ddf79d037b35634ecde68f33c217b4/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9", size = 2363318, upload-time = "2026-05-06T13:38:53.039Z" }, + { url = "https://files.pythonhosted.org/packages/62/88/60b110889507a426eecf626f7536566cb290ada71147eff49b6e2724ca62/pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1", size = 1988880, upload-time = "2026-05-06T13:39:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d6/8ede2f98f17e1e4e127d37be0eced4eee931a511c62cd68af50e1b25bfa9/pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac", size = 2079257, upload-time = "2026-05-06T13:39:38.498Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "starlette", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260513" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/f7/3228dd3794941bcb92ca6ca2045a6671a828ec0b47becbef23310bc45559/types_requests-2.33.0.20260513.tar.gz", hash = "sha256:bd845450e954e751373d5d33526742592f298808a3ee3bda7e858e46b839b57f", size = 24714, upload-time = "2026-05-13T05:39:23.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/f5/233a78be8367a9888de718f002fb27b1ea4be39471cd88aedeafceed872e/types_requests-2.33.0.20260513-py3-none-any.whl", hash = "sha256:d5a965f9d18b6e06b72039a69565de9027e58f36a7f709857da747fbe7521122", size = 21390, upload-time = "2026-05-13T05:39:22.262Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version >= '3.10'" }, + { name = "h11", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] From 45783ef6265d4d5de1a4e0c39da29413f5e2f9b1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 5 Jul 2026 17:55:07 +0200 Subject: [PATCH 02/94] feat(translator): Implement AG-UI<>OpenAI Agents SDK translation logic --- integrations/openai-agents/python/.gitignore | 7 +- integrations/openai-agents/python/README.md | 20 +- .../openai-agents/python/pyproject.toml | 6 + .../src/ag_ui_openai_agents/__init__.py | 30 + .../translator/__init__.py | 58 ++ .../translator/agui_to_sdk.py | 630 +++++++++++++ .../ag_ui_openai_agents/translator/helpers.py | 126 +++ .../translator/sdk_to_agui.py | 830 ++++++++++++++++++ .../translator/stream_types.py | 84 ++ .../ag_ui_openai_agents/translator/types.py | 114 +++ .../python/tests/test_stream_types_drift.py | 143 +++ integrations/openai-agents/python/uv.lock | 195 ++++ 12 files changed, 2241 insertions(+), 2 deletions(-) create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/translator/__init__.py create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/translator/agui_to_sdk.py create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/translator/stream_types.py create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/translator/types.py create mode 100644 integrations/openai-agents/python/tests/test_stream_types_drift.py diff --git a/integrations/openai-agents/python/.gitignore b/integrations/openai-agents/python/.gitignore index 30b72f3bd5..0807b9ba84 100644 --- a/integrations/openai-agents/python/.gitignore +++ b/integrations/openai-agents/python/.gitignore @@ -218,4 +218,9 @@ marimo/_lsp/ __marimo__/ # Streamlit -.streamlit/secrets.toml \ No newline at end of file +.streamlit/secrets.toml + +# Dev +.dev/ +CLAUDE.md +examples/ \ No newline at end of file diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 18c8c6845a..79d41ec84c 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -9,4 +9,22 @@ agent with the OpenAI SDK as usual, then stream its execution as AG-UI events. ```bash pip install ag-ui-openai-agent-sdk uv sync -``` \ No newline at end of file +``` + +## Testing + +```bash +uv sync # installs dev group (pytest) +uv run pytest # run the full suite +``` + +The suite includes a **drift guard** (`tests/test_stream_types_drift.py`): +this package hardcodes the wire `type` strings it dispatches on (in +`translator/stream_types.py`), and the guard asserts each one against the +`Literal[...]` annotations of the installed `openai-agents` / `openai` +packages. After bumping either dependency, run `uv run pytest` — if a wire +type was renamed or a new hosted tool-call item type was added, the guard +fails with an assertion diff naming the exact value to update in +`stream_types.py`. Unknown types never crash at runtime (the translator +degrades gracefully and skips them); the guard exists so drift is caught in +CI instead of silently dropping events. \ No newline at end of file diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml index 7697d56e79..b47cc0dfc7 100644 --- a/integrations/openai-agents/python/pyproject.toml +++ b/integrations/openai-agents/python/pyproject.toml @@ -11,6 +11,12 @@ dependencies = [ "ag-ui-protocol>=0.1.18", "openai-agents>=0.8.4", "pydantic>=2.13.4", + "jsonpatch>=1.33", +] + +[dependency-groups] +dev = [ + "pytest>=8.0", ] [build-system] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py new file mode 100644 index 0000000000..9874b2a731 --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py @@ -0,0 +1,30 @@ +"""OpenAI Agents SDK × AG-UI Protocol integration.""" + +from __future__ import annotations +# +# from .agent import OpenAIAgentsAgent +# from .endpoint import add_fastapi_endpoint, create_app +from .translator import ( + AGUIToSDKTranslator, + ClientToolPending, + SDKToAGUITranslator, + StateDiffer, + TranslatedInput, +) + +__version__ = "0.1.0" + +__all__ = [ + # Main agent wrapper + # "OpenAIAgentsAgent", + # # FastAPI wiring + # "add_fastapi_endpoint", + # "create_app", + # Translators (for advanced / manual use) + "AGUIToSDKTranslator", + "SDKToAGUITranslator", + # Types & helpers + "TranslatedInput", + "ClientToolPending", + "StateDiffer", +] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/__init__.py new file mode 100644 index 0000000000..433013b808 --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/__init__.py @@ -0,0 +1,58 @@ +""" +Bi-directional translation between AG-UI and the OpenAI Agents SDK. + +Two independent classes — one per direction — plus shared helpers and a +typed result container. Import what you need:: + + from ag_ui_openai_agents.translator import ( + AGUIToSDKTranslator, # inbound: AG-UI primitives → SDK shapes + SDKToAGUITranslator, # outbound: SDK formats → AG-UI primitives + TranslatedInput, # Pydantic bundle returned by translate() + ClientToolPending, # sentinel raised by client-tool proxies + StateDiffer, # JSON Patch helper for STATE_DELTA events + ) +""" + +from __future__ import annotations + +from .agui_to_sdk import AGUIToSDKTranslator, ClientToolPending +from .helpers import ( + StateDiffer, + coerce_to_str, + new_message_id, + new_tool_call_id, + new_tool_result_id, + read_attr, + snapshot_state, +) +from .sdk_to_agui import SDKToAGUITranslator +from .stream_types import ( + HOSTED_TOOL_CALL_TYPES, + RawResponseEventType, + SDKItemType, + SDKStreamEventType, +) +from .types import TranslatedInput + +__all__ = [ + # Translators + "AGUIToSDKTranslator", + "SDKToAGUITranslator", + # Result types + "TranslatedInput", + # Wire discriminators + "SDKStreamEventType", + "RawResponseEventType", + "SDKItemType", + "HOSTED_TOOL_CALL_TYPES", + # Sentinels + "ClientToolPending", + # Shared helpers + "StateDiffer", + "snapshot_state", + "new_message_id", + "new_tool_call_id", + "new_tool_result_id", + "read_attr", + "coerce_to_str", +] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/agui_to_sdk.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/agui_to_sdk.py new file mode 100644 index 0000000000..5f870c2e81 --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/agui_to_sdk.py @@ -0,0 +1,630 @@ +"""Inbound translator: AG-UI request primitives → OpenAI Agents SDK formats. + +Layered API (each tier is callable on its own — they build on each other): + + Tier 1 — One-shot: + translate(run_input) → TranslatedInput (everything wired up) + + Tier 2 — Bulk collections: + translate_messages(messages) → list[input item] + translate_tools(tools) → list[FunctionTool] + translate_context(items) → str (formatted text block) + + Tier 3 — Single items (per AG-UI type): + translate_message(msg) → list[input item] (dispatcher) + translate_user_message + translate_system_message + translate_developer_message + translate_assistant_message + translate_tool_message + translate_reasoning_message + translate_activity_message + translate_tool(tool) → FunctionTool + translate_content(content) → list[input part] (dispatcher) + translate_content_part(part) → input part | None (dispatcher) + translate_text_content + translate_image_content + translate_audio_content + translate_video_content + translate_document_content + translate_binary_content + + Tier 4 — Internal helpers (underscore-prefixed). + +The translator is **stateless** — instantiate it once and reuse, or call +methods on a throwaway instance per request. Either works. +""" + +from __future__ import annotations + +import logging +from typing import Any, Iterable + +from ag_ui.core import ( + ActivityMessage, + AssistantMessage, + AudioInputContent, + BinaryInputContent, + Context, + DeveloperMessage, + DocumentInputContent, + ImageInputContent, + Message, + ReasoningMessage, + RunAgentInput, + SystemMessage, + TextInputContent, + Tool as AGUITool, + ToolMessage, + UserMessage, + VideoInputContent, +) +from agents import FunctionTool, RunContextWrapper, TResponseInputItem + +from .helpers import coerce_to_str, read_attr +from .types import TranslatedInput + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Sentinel exception used by client-tool proxies +# --------------------------------------------------------------------------- + +class ClientToolPending(Exception): + """Raised by a client-tool proxy to signal "stop, the UI owns this call". + + The outer run loop catches it, cancels the SDK run after the current turn, + and persists the resulting ``RunState`` keyed by ``thread_id`` so the next + AG-UI request (which carries an AG-UI ``ToolMessage`` with the client's + result) can resume from the same point. + """ + + def __init__(self, tool_name: str, tool_call_id: str, arguments: str) -> None: + super().__init__( + f"Client tool '{tool_name}' (call_id={tool_call_id}) pending UI execution" + ) + self.tool_name = tool_name + self.tool_call_id = tool_call_id + self.arguments = arguments + + +# --------------------------------------------------------------------------- +# The translator +# --------------------------------------------------------------------------- + +class AGUIToSDKTranslator: + """Translate AG-UI inbound primitives into shapes the OpenAI Agents SDK expects. + + Examples + -------- + One-shot:: + + bundle = AGUIToSDKTranslator().translate(run_input) + result = Runner.run_streamed( + agent.clone(tools=agent.tools + bundle.function_tools), + input=bundle.input_items, + ) + + Per-item:: + + translator = AGUIToSDKTranslator() + items = [translator.translate_user_message(msg) for msg in user_msgs] + proxy = translator.translate_tool(my_tool) + image_part = translator.translate_image_content(part) + """ + + # ───────────────────────────────────────────────────────────────────── + # TIER 1 — One-shot entry point + # ───────────────────────────────────────────────────────────────────── + + def translate(self, run_input: RunAgentInput) -> TranslatedInput: + """ + Translate an entire ``RunAgentInput`` into an SDK-ready bundle. + + This is the high-level entry point. It does everything: converts + messages, wraps tools, and forwards state / context / forwarded_props / + thread_id / run_id / parent_run_id / resume so callers have one object + that mirrors :class:`ag_ui.core.RunAgentInput` field-for-field. + + Note on ``context`` and ``resume``: both pass through **unchanged**. + ``context`` items (from ``useCopilotReadable`` etc.) are not auto-folded + into the system prompt — call :meth:`translate_context` to format them + if your agent needs the model to see them. + """ + # ``resume`` was added in the TypeScript SDK first; the Python SDK + # may not expose it yet — read defensively so older SDK versions + # don't break us. + resume = getattr(run_input, "resume", None) + return TranslatedInput( + thread_id=run_input.thread_id, + run_id=run_input.run_id, + parent_run_id=getattr(run_input, "parent_run_id", None), + messages=self.translate_messages(run_input.messages or []), + # tools=self.translate_tools(run_input.tools or []), + state=run_input.state, + context=list(run_input.context or []), + forwarded_props=run_input.forwarded_props, + resume=list(resume) if resume else None, + ) + + # ───────────────────────────────────────────────────────────────────── + # TIER 2 — Bulk collections + # ───────────────────────────────────────────────────────────────────── + + def translate_messages( + self, + messages: Iterable[Message], + ) -> list[TResponseInputItem]: + """Translate every message and flatten into one Responses-API input list.""" + items = [] + for message in messages: + items.extend(self.translate_message(message)) + return items + + def translate_tools( + self, + tools: Iterable[AGUITool], + ) -> list[FunctionTool]: + """Wrap every AG-UI tool as a long-running SDK :class:`FunctionTool` proxy.""" + return [self.translate_tool(tool) for tool in tools] + + def translate_context( + self, + items: Iterable[Context], + ) -> str: + """Render ambient context items as a plain-text block for the system prompt. + + AG-UI ``context`` carries ``{description, value}`` pairs that frontends + (CopilotKit's ``useCopilotReadable``, etc.) send to give the model + ambient knowledge about the user's UI state — current page, selected + item, user identity, etc. + + We do **not** auto-inject this anywhere — callers decide whether to + prepend the rendered string to a system message, store it in agent + state, or ignore it. Returns an empty string when the input is empty. + + Output format:: + + Description A: value A + Description B: value B + + Example:: + + prompt = "You are helpful." + ctx = translator.translate_context(bundle.context) + if ctx: + prompt = f"{prompt}\\n\\nContext:\\n{ctx}" + """ + lines = [ + f"{item.description}: {item.value}" + for item in items + if item.description or item.value + ] + return "\n".join(lines) + + # ───────────────────────────────────────────────────────────────────── + # TIER 3a — Single message (dispatcher + per-type) + # ───────────────────────────────────────────────────────────────────── + + def translate_message(self, message: Message) -> list[dict[str, Any]]: + """Dispatch one AG-UI message to the right per-type translator. + + Returns a **list** because one message can produce multiple Responses-API + items (an AssistantMessage with N tool_calls splits into 1 message item + + N function_call items). + """ + if isinstance(message, UserMessage): + return [self.translate_user_message(message)] + if isinstance(message, SystemMessage): + return [self.translate_system_message(message)] + if isinstance(message, DeveloperMessage): + return [self.translate_developer_message(message)] + if isinstance(message, AssistantMessage): + return self.translate_assistant_message(message) + if isinstance(message, ToolMessage): + return [self.translate_tool_message(message)] + if isinstance(message, ReasoningMessage): + item = self.translate_reasoning_message(message) + return [item] if item is not None else [] + if isinstance(message, ActivityMessage): + item = self.translate_activity_message(message) + return [item] if item is not None else [] + logger.debug("Unknown AG-UI message type: %s", type(message).__name__) + return [] + + def translate_user_message(self, message: UserMessage) -> dict[str, Any]: + """User turn → ``{"type": "message", "role": "user", "content": [...]}``. + + Supports multimodal: ``message.content`` may be a string or a list of + typed content parts (text / image / audio / ...). + """ + return { + "type": "message", + "role": "user", + "content": self.translate_content(message.content), + } + + def translate_system_message(self, message: SystemMessage) -> dict[str, Any]: + """System prompt → ``{"type": "message", "role": "system", ...}``.""" + return { + "type": "message", + "role": "system", + "content": [{"type": "input_text", "text": message.content or ""}], + } + + def translate_developer_message(self, message: DeveloperMessage) -> dict[str, Any]: + """Developer prompt → ``{"type": "message", "role": "developer", ...}``. + + OpenAI's newer model lineage (GPT-4o+, o1) accepts ``role: developer`` + as a higher-priority alternative to ``system``. + """ + return { + "type": "message", + "role": "developer", + "content": [{"type": "input_text", "text": message.content or ""}], + } + + def translate_assistant_message( + self, + message: AssistantMessage, + ) -> list[dict[str, Any]]: + """Assistant turn → optional text message + one ``function_call`` per tool call. + + The Responses API splits assistant tool calls into separate items + (one ``message`` for any spoken text, plus one ``function_call`` per + invocation) — so one AG-UI message can produce N+1 items. + """ + items: list[dict[str, Any]] = [] + text = message.content or "" + if text: + items.append( + { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": text}], + } + ) + for tool_call in message.tool_calls or []: + items.append( + { + "type": "function_call", + "call_id": tool_call.id, + "name": tool_call.function.name, + "arguments": tool_call.function.arguments or "", + } + ) + return items + + def translate_tool_message(self, message: ToolMessage) -> dict[str, Any]: + """Tool result → ``{"type": "function_call_output", ...}``.""" + return { + "type": "function_call_output", + "call_id": message.tool_call_id, + "output": message.content or "", + } + + def translate_reasoning_message( + self, + message: ReasoningMessage, + ) -> dict[str, Any] | None: + """Reasoning trace → ``{"type": "reasoning", ...}`` if usable, else ``None``. + + OpenAI o-series models can re-ingest *encrypted* reasoning blobs (the + ``encrypted_value`` field) but treat plaintext reasoning as opaque. + We only emit a reasoning item when an encrypted value is present; + otherwise we drop the message with a debug log. + """ + if not message.encrypted_value: + logger.debug( + "Dropping ReasoningMessage id=%s: no encrypted_value to re-ingest", + message.id, + ) + return None + return { + "type": "reasoning", + "id": message.id, + "encrypted_content": message.encrypted_value, + "summary": [{"type": "summary_text", "text": message.content or ""}], + } + + def translate_activity_message( + self, + message: ActivityMessage, + ) -> dict[str, Any] | None: + """Activity status → dropped by default (no Responses-API equivalent). + + Activity messages describe UI side-effects (e.g. "user navigated") that + the model doesn't need to ingest. Subclasses can override to fold them + into the system prompt if a particular use case needs it. + """ + logger.debug( + "Dropping ActivityMessage id=%s activity_type=%s", + message.id, + message.activity_type, + ) + return None + + # ───────────────────────────────────────────────────────────────────── + # TIER 3b — Single tool + # ───────────────────────────────────────────────────────────────────── + + def translate_tool(self, tool: AGUITool) -> FunctionTool: + """ + Wrap one AG-UI :class:`Tool` as an SDK :class:`FunctionTool` proxy. + + The proxy's invocation handler raises :class:`ClientToolPending` so the + outer run loop can pause the SDK and hand control back to the client. + """ + schema = self._ensure_object_schema(tool.parameters) + + async def on_invoke_tool( + ctx: RunContextWrapper[Any], + arguments_json: str, + ) -> str: + call_id = getattr(ctx, "tool_call_id", None) or "" + raise ClientToolPending( + tool_name=tool.name, + tool_call_id=call_id, + arguments=arguments_json or "", + ) + + return FunctionTool( + name=tool.name, + description=tool.description or "", + params_json_schema=schema, + on_invoke_tool=on_invoke_tool, + strict_json_schema=False, + ) + + # ───────────────────────────────────────────────────────────────────── + # TIER 3c — Content parts (multimodal) + # ───────────────────────────────────────────────────────────────────── + + def translate_content(self, content: Any) -> list[dict[str, Any]]: + """Translate a message ``content`` field (str or list of parts) to input parts. + + * String → wrapped as a single ``input_text`` part. + * List → each part passed through :meth:`translate_content_part`. + * Anything else → best-effort stringification. + """ + if isinstance(content, str): + return [{"type": "input_text", "text": content}] + if isinstance(content, list): + parts: list[dict[str, Any]] = [] + for part in content: + converted = self.translate_content_part(part) + if converted is not None: + parts.append(converted) + if parts: + return parts + return [{"type": "input_text", "text": coerce_to_str(content)}] + + def translate_content_part(self, part: Any) -> dict[str, Any] | None: + """Dispatch one content part to its per-type translator.""" + if isinstance(part, TextInputContent): + return self.translate_text_content(part) + if isinstance(part, ImageInputContent): + return self.translate_image_content(part) + if isinstance(part, AudioInputContent): + return self.translate_audio_content(part) + if isinstance(part, VideoInputContent): + return self.translate_video_content(part) + if isinstance(part, DocumentInputContent): + return self.translate_document_content(part) + if isinstance(part, BinaryInputContent): + return self.translate_binary_content(part) + # Duck-typed fallback for dict-style parts (e.g. from JSON without strict parsing). + return self._dispatch_dict_content_part(part) + + def translate_text_content(self, part: TextInputContent) -> dict[str, Any]: + """``TextInputContent`` → ``{"type": "input_text", "text": ...}``.""" + return {"type": "input_text", "text": part.text or ""} + + def translate_image_content( + self, + part: ImageInputContent, + ) -> dict[str, Any] | None: + """``ImageInputContent`` → ``{"type": "input_image", "image_url": ...}``. + + URL sources pass through unchanged. Data sources become base64 data URLs + so the Responses-API ``image_url`` field always receives a single string. + """ + url = self._data_source_to_url(part.source) + if url is None: + return None + return {"type": "input_image", "image_url": url} + + def translate_audio_content( + self, + part: AudioInputContent, + ) -> dict[str, Any] | None: + """``AudioInputContent`` → ``{"type": "input_audio", "input_audio": {...}}``. + + The Responses API accepts base64 audio data with a short format tag + (``wav``, ``mp3``). We extract the format from the mime type. URL + sources are not supported by the API for audio — they get dropped. + """ + source_type = read_attr(part.source, "type") + value = read_attr(part.source, "value") + mime = read_attr(part.source, "mime_type") + if not value or source_type != "data": + logger.debug("Dropping audio part: only data sources are supported") + return None + return { + "type": "input_audio", + "input_audio": { + "data": value, + "format": self._audio_format_from_mime(mime), + }, + } + + def translate_video_content( + self, + part: VideoInputContent, + ) -> dict[str, Any] | None: + """``VideoInputContent`` → dropped (Responses API has no native video input). + + Override this in a subclass to swap in a placeholder or extract frames. + """ + logger.debug("Dropping video part: Responses API does not accept video input") + return None + + def translate_document_content( + self, + part: DocumentInputContent, + ) -> dict[str, Any] | None: + """``DocumentInputContent`` → ``{"type": "input_file", ...}``. + + URL sources use ``file_url``; data sources use ``file_data`` (base64). + """ + source_type = read_attr(part.source, "type") + value = read_attr(part.source, "value") + mime = read_attr(part.source, "mime_type") + if not value: + return None + if source_type == "url": + return {"type": "input_file", "file_url": value} + if source_type == "data": + return { + "type": "input_file", + "file_data": f"data:{mime or 'application/octet-stream'};base64,{value}", + } + return None + + def translate_binary_content( + self, + part: BinaryInputContent, + ) -> dict[str, Any] | None: + """``BinaryInputContent`` → routed by mime type. + + Binary parts are a polymorphic catch-all: they may be images, audio, + documents, etc. We sniff the mime type and route to the corresponding + Responses-API input shape. Unknown types are dropped. + """ + mime = part.mime_type or "application/octet-stream" + + if mime.startswith("image/"): + return self._binary_as_image(part) + if mime.startswith("audio/"): + return self._binary_as_audio(part, mime) + # Treat anything else (pdf, text, application/*) as a file. + return self._binary_as_file(part, mime) + + # ───────────────────────────────────────────────────────────────────── + # TIER 4 — Internal helpers + # ───────────────────────────────────────────────────────────────────── + + def _ensure_object_schema(self, parameters: Any) -> dict[str, Any]: + """Normalise a possibly-empty tool parameter spec into a JSON Schema object. + + The Responses API requires every function tool's schema to be a JSON + Schema object. AG-UI tools sometimes ship parameter-less specs as + ``None`` or ``{}``; we coerce those to an empty-but-valid object. + """ + if not isinstance(parameters, dict) or "type" not in parameters: + return {"type": "object", "properties": {}, "additionalProperties": True} + return parameters + + @staticmethod + def _data_source_to_url(source: Any) -> str | None: + """Render a content source (data or url) as a single string for ``image_url``. + + * URL sources → pass through. + * Data sources → ``data:;base64,``. + """ + if source is None: + return None + source_type = read_attr(source, "type") + value = read_attr(source, "value") + if not value: + return None + if source_type == "url": + return value + if source_type == "data": + mime = read_attr(source, "mime_type") or "application/octet-stream" + return f"data:{mime};base64,{value}" + return None + + @staticmethod + def _audio_format_from_mime(mime: str | None) -> str: + """Map a mime type to the short format string the Responses API wants.""" + if not mime: + return "wav" + subtype = mime.split("/", 1)[-1].lower() + # Common normalisations. + if subtype in ("mpeg", "mpeg3", "mp3"): + return "mp3" + if subtype in ("x-wav", "wav", "wave"): + return "wav" + return subtype + + def _dispatch_dict_content_part( + self, + part: Any, + ) -> dict[str, Any] | None: + """Best-effort dispatch for dict-shaped content parts (loose typing). + + Sometimes content arrives as raw dicts rather than pydantic objects + (e.g. when callers hand-craft inputs). We sniff ``type`` and route. + """ + part_type = read_attr(part, "type") + if part_type == "text": + text = read_attr(part, "text") + if text is None: + return None + return {"type": "input_text", "text": text} + if part_type == "image": + url = self._data_source_to_url(read_attr(part, "source")) + return {"type": "input_image", "image_url": url} if url else None + # Unknown / unsupported. + return None + + # -- Binary-routing sub-helpers (internal, used only by translate_binary_content) + + def _binary_as_image(self, part: BinaryInputContent) -> dict[str, Any] | None: + if part.url: + return {"type": "input_image", "image_url": part.url} + if part.data: + mime = part.mime_type or "application/octet-stream" + return {"type": "input_image", "image_url": f"data:{mime};base64,{part.data}"} + return None + + def _binary_as_audio( + self, + part: BinaryInputContent, + mime: str, + ) -> dict[str, Any] | None: + if not part.data: + logger.debug("Dropping binary audio: URL-only audio not supported") + return None + return { + "type": "input_audio", + "input_audio": { + "data": part.data, + "format": self._audio_format_from_mime(mime), + }, + } + + def _binary_as_file( + self, + part: BinaryInputContent, + mime: str, + ) -> dict[str, Any] | None: + if part.url: + return {"type": "input_file", "file_url": part.url} + if part.data: + payload = { + "type": "input_file", + "file_data": f"data:{mime};base64,{part.data}", + } + if part.filename: + payload["filename"] = part.filename + return payload + return None + + +__all__ = [ + "AGUIToSDKTranslator", + "ClientToolPending", +] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py new file mode 100644 index 0000000000..07cfe4017d --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py @@ -0,0 +1,126 @@ +"""Internal helpers shared by both translator directions. + +These are deliberately tiny, side-effect-free, and dependency-light so they +can be reused by either translator without coupling them. +""" + +from __future__ import annotations + +import copy +import json +import uuid +from typing import Any + +import jsonpatch + + +# --------------------------------------------------------------------------- +# ID generation +# --------------------------------------------------------------------------- + +def new_message_id() -> str: + """Generate a fresh AG-UI message id (``msg_``).""" + return f"msg_{uuid.uuid4().hex}" + + +def new_tool_call_id() -> str: + """Generate a fresh tool call id (``call_``).""" + return f"call_{uuid.uuid4().hex}" + + +def new_tool_result_id() -> str: + """Generate a fresh tool result id (``toolresult_``).""" + return f"toolresult_{uuid.uuid4().hex}" + + +# --------------------------------------------------------------------------- +# Polymorphic attribute / value reading +# --------------------------------------------------------------------------- + +def read_attr(obj: Any, name: str) -> Any: + """Read ``name`` from a pydantic model **or** a dict — whichever we got. + + The SDK and AG-UI both hand us a mix of dicts and pydantic objects depending + on whether something has been through JSON serialization yet. This shields + callers from caring. + """ + if obj is None: + return None + if isinstance(obj, dict): + return obj.get(name) + return getattr(obj, name, None) + + +def coerce_to_str(value: Any) -> str: + """Best-effort stringification for tool outputs and content payloads.""" + if value is None: + return "" + if isinstance(value, str): + return value + try: + return json.dumps(value, default=str) + except (TypeError, ValueError): + return str(value) + + +# --------------------------------------------------------------------------- +# State diffing (JSON Patch RFC 6902) +# --------------------------------------------------------------------------- + +def snapshot_state(state: dict[str, Any]) -> dict[str, Any]: + """Deep-copy ``state`` so later mutations don't pollute the baseline.""" + return copy.deepcopy(state) + + +class StateDiffer: + """Tracks a baseline state and yields JSON Patch ops when it changes. + + Used by the outbound translator (and the agent loop) to surface mutations + that tools/instructions made to ``AGUIContext.state`` as AG-UI + ``STATE_DELTA`` events. + + Lifecycle:: + + differ = StateDiffer(initial=context.state) + # ... after each SDK event ... + ops = differ.diff(context.state) + if ops is not None: + yield StateDeltaEvent(type=..., delta=ops) + """ + + def __init__(self, initial: dict[str, Any]) -> None: + self._baseline: dict[str, Any] = snapshot_state(initial) + + def diff(self, current: dict[str, Any]) -> list[dict[str, Any]] | None: + """Return JSON Patch ops if ``current`` differs from the baseline. + + Returns ``None`` (not ``[]``) when nothing changed — so callers can use + a simple ``if ops is not None`` check before emitting an event. + After a non-empty diff, the baseline advances to ``current``. + """ + patch = jsonpatch.make_patch(self._baseline, current) + ops = list(patch) + if not ops: + return None + self._baseline = snapshot_state(current) + return ops + + @property + def baseline(self) -> dict[str, Any]: + """Return a copy of the current baseline (defensive).""" + return snapshot_state(self._baseline) + + def reset(self, state: dict[str, Any]) -> None: + """Force-reset the baseline to ``state`` without emitting a diff.""" + self._baseline = snapshot_state(state) + + +__all__ = [ + "new_message_id", + "new_tool_call_id", + "new_tool_result_id", + "read_attr", + "coerce_to_str", + "snapshot_state", + "StateDiffer", +] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py new file mode 100644 index 0000000000..a38d9238a5 --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py @@ -0,0 +1,830 @@ +"""Outbound translator: OpenAI Agents SDK events → AG-UI ``BaseEvent``. + +Layered API (each tier is callable on its own — they build on each other), +mirroring :class:`~.agui_to_sdk.AGUIToSDKTranslator`: + + Tier 1 — One-shot: + translate(sdk_event) → list[BaseEvent] (dispatcher) + finalize() → list[BaseEvent] (flush open windows) + + Tier 2 — Bulk / per event family: + translate_items(items) → list[BaseEvent] (non-streaming runs) + translate_raw_response_event(e) → list[BaseEvent] + translate_run_item_event(e) → list[BaseEvent] + translate_agent_updated_event(e) → list[BaseEvent] + + Tier 3a — Raw Responses deltas (per raw ``type``): + translate_output_item_added / translate_output_item_done + translate_text_delta / translate_refusal_delta / translate_text_done + translate_function_call_arguments_delta + translate_reasoning_delta / translate_reasoning_part_done + + Tier 3b — Single run item (per SDK type): + translate_item(item) → list[BaseEvent] (dispatcher) + translate_message_output_item + translate_tool_call_item + translate_tool_call_output_item + translate_reasoning_item + translate_handoff_call_item + translate_handoff_output_item + translate_mcp_approval_request_item + translate_mcp_list_tools_item + translate_mcp_approval_response_item + + Tier 4 — Internal helpers (underscore-prefixed). + +The translator is **stateful per run** — it tracks open text / tool-call / +reasoning windows so AG-UI always sees strict ``START → CONTENT → END`` +triplets. Instantiate a fresh one per AG-UI run; never share across runs. + +Works with every SDK execution mode: + + Streaming (async):: + + translator = SDKToAGUITranslator() + result = Runner.run_streamed(agent, input=items) + async for sdk_event in result.stream_events(): + for event in translator.translate(sdk_event): + yield event + for event in translator.finalize(): + yield event + + Non-streaming (async or sync):: + + result = await Runner.run(agent, input=items) # or Runner.run_sync(...) + for event in SDKToAGUITranslator().translate_items(result.new_items): + yield event + +Run-envelope events (``RUN_STARTED`` / ``RUN_FINISHED`` / ``RUN_ERROR``, +``STATE_SNAPSHOT`` / ``STATE_DELTA``, ``MESSAGES_SNAPSHOT``) are the run +loop's job — the translator only translates. +""" + +from __future__ import annotations + +import logging +from typing import Any + +from ag_ui.core import ( + BaseEvent, + CustomEvent, + EventType, + ReasoningEncryptedValueEvent, + ReasoningEndEvent, + ReasoningMessageContentEvent, + ReasoningMessageEndEvent, + ReasoningMessageStartEvent, + ReasoningStartEvent, + StepFinishedEvent, + StepStartedEvent, + TextMessageContentEvent, + TextMessageEndEvent, + TextMessageStartEvent, + ToolCallArgsEvent, + ToolCallEndEvent, + ToolCallResultEvent, + ToolCallStartEvent, +) +from agents import ( + HandoffCallItem, + HandoffOutputItem, + ItemHelpers, + MCPApprovalRequestItem, + MCPApprovalResponseItem, + MessageOutputItem, + ReasoningItem, + RunItem, + ToolCallItem, + ToolCallOutputItem, +) +from agents.items import MCPListToolsItem # not re-exported at package top level +from agents.models.fake_id import FAKE_RESPONSES_ID + +from .helpers import ( + coerce_to_str, + new_message_id, + new_tool_call_id, + new_tool_result_id, + read_attr, +) +from .stream_types import ( + HOSTED_TOOL_CALL_TYPES, + RawResponseEventType, + SDKItemType, + SDKStreamEventType, +) + +logger = logging.getLogger(__name__) + + +class SDKToAGUITranslator: + """Translate OpenAI Agents SDK outputs into AG-UI :class:`BaseEvent` objects. + + Wire ids are reused, never invented — with one caveat: some model backends + stamp every item with the SDK's ``FAKE_RESPONSES_ID`` placeholder instead + of a real id. Windows are therefore keyed by real item + id when available and by the raw event's ``output_index`` otherwise, and + placeholder ids are replaced with generated ones so AG-UI clients never see + two different messages sharing an id. + + The pattern is "lazy open, eager close": a ``*_START`` is emitted when the + first signal for a window arrives (``output_item.added`` or, defensively, + a bare delta), and ``*_END`` fires on the first close signal — + ``output_item.done``, the run-item commit, or :meth:`finalize`, whichever + comes first. Run items double as the full emission path for non-streaming + runs: when no window was ever opened for an item, its run-item translator + emits the complete triplet from the finished item. + """ + + def __init__(self) -> None: + # Open windows, keyed by real item id or ``__idx_``. + # Values are the ids already emitted in the *_START events. + self._open_texts: dict[str, str] = {} # key → message_id + self._open_tool_calls: dict[str, str] = {} # key → tool_call_id + self._open_reasonings: dict[str, str] = {} # key → phase message_id + self._open_reasoning_parts: dict[str, str] = {} # key → part message_id + # Close bookkeeping — lets run items tell "close me" (streamed) apart + # from "emit me whole" (non-streamed) even with placeholder ids. + self._closed_text_ids: list[str] = [] + self._closed_reasoning_ids: list[str] = [] + self._seen_call_ids: set[str] = set() + self._emitted_encrypted_keys: set[str] = set() + self._reasoning_part_seq: dict[str, int] = {} + # key → phase id, never popped — encrypted values can arrive after the + # phase was already force-closed and still need the right entity_id. + self._reasoning_phase_ids: dict[str, str] = {} + self._current_step: str | None = None + + # ───────────────────────────────────────────────────────────────────── + # TIER 1 — One-shot entry points + # ───────────────────────────────────────────────────────────────────── + + def translate(self, sdk_event: Any) -> list[BaseEvent]: + """Dispatch one SDK :class:`StreamEvent` to the right family translator. + + Returns a **list** because one SDK event can produce several AG-UI + events (e.g. an ``output_item.added`` that force-opens a window). + Unknown event types translate to ``[]`` with a debug log. + """ + event_type = read_attr(sdk_event, "type") + if event_type == SDKStreamEventType.RAW_RESPONSE: + return self.translate_raw_response_event(sdk_event) + if event_type == SDKStreamEventType.RUN_ITEM: + return self.translate_run_item_event(sdk_event) + if event_type == SDKStreamEventType.AGENT_UPDATED: + return self.translate_agent_updated_event(sdk_event) + logger.debug("Unknown SDK stream event type: %s", event_type) + return [] + + def finalize(self) -> list[BaseEvent]: + """Emit pending ``*_END`` markers when the SDK stream terminates. + + Always call this after the stream ends — even on error — so the AG-UI + client never sees a window that opened but never closed. + """ + events: list[BaseEvent] = [] + for key in list(self._open_texts): + events.extend(self._close_text(key)) + for key in list(self._open_tool_calls): + events.extend(self._close_tool_call(key)) + for key in list(self._open_reasonings): + events.extend(self._close_reasoning(key)) + if self._current_step is not None: + events.append( + StepFinishedEvent( + type=EventType.STEP_FINISHED, + step_name=self._current_step, + ) + ) + self._current_step = None + return events + + # ───────────────────────────────────────────────────────────────────── + # TIER 2 — Bulk collection + per event family + # ───────────────────────────────────────────────────────────────────── + + def translate_items(self, items: list[RunItem]) -> list[BaseEvent]: + """Translate a finished run's items (non-streaming mode). + + Feed ``result.new_items`` from ``Runner.run`` / ``Runner.run_sync``. + Each item emits its complete AG-UI sequence (full triplets — no + windows stay open), so no :meth:`finalize` call is needed after. + """ + events: list[BaseEvent] = [] + for item in items: + events.extend(self.translate_item(item)) + return events + + def translate_raw_response_event(self, event: Any) -> list[BaseEvent]: + """Handle a ``RawResponsesStreamEvent`` (token-level Responses deltas).""" + data = read_attr(event, "data") + if data is None: + return [] + kind = read_attr(data, "type") + if kind == RawResponseEventType.OUTPUT_ITEM_ADDED: + return self.translate_output_item_added(data) + if kind == RawResponseEventType.OUTPUT_ITEM_DONE: + return self.translate_output_item_done(data) + if kind == RawResponseEventType.TEXT_DELTA: + return self.translate_text_delta(data) + if kind == RawResponseEventType.TEXT_DONE: + return self.translate_text_done(data) + if kind == RawResponseEventType.REFUSAL_DELTA: + return self.translate_refusal_delta(data) + if kind == RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA: + return self.translate_function_call_arguments_delta(data) + if kind in ( + RawResponseEventType.REASONING_SUMMARY_DELTA, + RawResponseEventType.REASONING_TEXT_DELTA, + ): + return self.translate_reasoning_delta(data) + if kind in ( + RawResponseEventType.REASONING_SUMMARY_PART_DONE, + RawResponseEventType.REASONING_TEXT_DONE, + ): + return self.translate_reasoning_part_done(data) + # response.created / .completed / content_part bookkeeping / audio — no + # AG-UI equivalent at this layer. + return [] + + def translate_run_item_event(self, event: Any) -> list[BaseEvent]: + """Handle a ``RunItemStreamEvent`` (semantic commit signals).""" + item = read_attr(event, "item") + if item is None: + return [] + return self.translate_item(item) + + def translate_agent_updated_event(self, event: Any) -> list[BaseEvent]: + """``AgentUpdatedStreamEvent`` → ``STEP_FINISHED`` (prev) + ``STEP_STARTED``. + + Each agent (including the first, and each handoff target) is surfaced + as an AG-UI step named after the agent. + """ + new_agent = read_attr(event, "new_agent") + step_name = read_attr(new_agent, "name") or "agent" + events: list[BaseEvent] = [] + if self._current_step is not None: + events.append( + StepFinishedEvent( + type=EventType.STEP_FINISHED, + step_name=self._current_step, + ) + ) + self._current_step = step_name + events.append( + StepStartedEvent(type=EventType.STEP_STARTED, step_name=step_name) + ) + return events + + # ───────────────────────────────────────────────────────────────────── + # TIER 3a — Raw Responses deltas (per raw ``type``) + # ───────────────────────────────────────────────────────────────────── + + def translate_output_item_added(self, data: Any) -> list[BaseEvent]: + """``response.output_item.added`` → open the matching AG-UI window. + + * ``message`` → ``TEXT_MESSAGE_START`` + * ``function_call`` → ``TOOL_CALL_START`` + * ``reasoning`` → ``REASONING_START`` + * hosted tool calls → ``TOOL_CALL_START`` (tool name = item type) + """ + item = read_attr(data, "item") + item_type = read_attr(item, "type") + item_id = read_attr(item, "id") + key = self._window_key(item_id, read_attr(data, "output_index")) + + if item_type == SDKItemType.MESSAGE: + return self._open_text(key, self._resolve_id(item_id, new_message_id)) + if item_type == SDKItemType.FUNCTION_CALL: + call_id = read_attr(item, "call_id") or self._resolve_id(item_id, new_tool_call_id) + name = read_attr(item, "name") or "" + return self._open_tool_call(key, call_id, name) + if item_type == SDKItemType.REASONING: + return self._open_reasoning(key, self._resolve_id(item_id, new_message_id)) + if item_type in HOSTED_TOOL_CALL_TYPES: + call_id = self._resolve_id(item_id, new_tool_call_id) + return self._open_tool_call(key, call_id, item_type) + return [] + + def translate_output_item_done(self, data: Any) -> list[BaseEvent]: + """``response.output_item.done`` → close the matching AG-UI window. + + For reasoning items this also emits ``REASONING_ENCRYPTED_VALUE`` + (subtype ``"message"``) when the finished item carries + ``encrypted_content``. + """ + item = read_attr(data, "item") + item_type = read_attr(item, "type") + key = self._window_key(read_attr(item, "id"), read_attr(data, "output_index")) + + if item_type == SDKItemType.MESSAGE: + return self._close_text(key) + if item_type == SDKItemType.FUNCTION_CALL or item_type in HOSTED_TOOL_CALL_TYPES: + return self._close_tool_call(key) + if item_type == SDKItemType.REASONING: + events = self._emit_encrypted_value(key, item) + events.extend(self._close_reasoning(key)) + return events + return [] + + def translate_text_delta(self, data: Any) -> list[BaseEvent]: + """``response.output_text.delta`` → ``TEXT_MESSAGE_CONTENT`` (lazy start).""" + key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) + return self._emit_text_content(key, read_attr(data, "delta") or "") + + def translate_text_done(self, data: Any) -> list[BaseEvent]: + """``response.output_text.done`` → early ``TEXT_MESSAGE_END``. + + Some model backends skip ``output_item.done``; this closes the + window on the text-level done signal instead. Idempotent — closing an + already-closed window is a no-op. + """ + key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) + return self._close_text(key) + + def translate_refusal_delta(self, data: Any) -> list[BaseEvent]: + """``response.refusal.delta`` → ``TEXT_MESSAGE_CONTENT``. + + Refusal text is user-visible assistant output, so it streams into the + same text window as regular content. + """ + key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) + return self._emit_text_content(key, read_attr(data, "delta") or "") + + def translate_function_call_arguments_delta(self, data: Any) -> list[BaseEvent]: + """``response.function_call_arguments.delta`` → ``TOOL_CALL_ARGS``.""" + delta = read_attr(data, "delta") or "" + if not delta: + return [] + key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) + events: list[BaseEvent] = [] + if key not in self._open_tool_calls: + # Defensive lazy open — provider skipped output_item.added. + events.extend(self._open_tool_call(key, new_tool_call_id(), "")) + events.append( + ToolCallArgsEvent( + type=EventType.TOOL_CALL_ARGS, + tool_call_id=self._open_tool_calls[key], + delta=delta, + ) + ) + return events + + def translate_reasoning_delta(self, data: Any) -> list[BaseEvent]: + """Reasoning deltas → ``REASONING_MESSAGE_CONTENT`` (lazy part start). + + Covers both sources — ``response.reasoning_summary_text.delta`` + (hosted models: model-written summaries) and + ``response.reasoning_text.delta`` (open-weight models: full chain of + thought). Whichever arrives streams; each part becomes its own + ``REASONING_MESSAGE_*`` window inside one ``REASONING_START/END`` + phase, matching the other AG-UI integrations. + """ + delta = read_attr(data, "delta") or "" + if not delta: + return [] + key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) + events: list[BaseEvent] = [] + if key not in self._open_reasonings: + events.extend(self._open_reasoning(key, new_message_id())) + if key not in self._open_reasoning_parts: + events.extend(self._open_reasoning_part(key)) + events.append( + ReasoningMessageContentEvent( + type=EventType.REASONING_MESSAGE_CONTENT, + message_id=self._open_reasoning_parts[key], + delta=delta, + ) + ) + return events + + def translate_reasoning_part_done(self, data: Any) -> list[BaseEvent]: + """Summary-part / reasoning-text done → ``REASONING_MESSAGE_END``.""" + key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) + return self._close_reasoning_part(key) + + # ───────────────────────────────────────────────────────────────────── + # TIER 3b — Single run item (dispatcher + per-type) + # ───────────────────────────────────────────────────────────────────── + + def translate_item(self, item: RunItem) -> list[BaseEvent]: + """Dispatch one SDK :class:`RunItem` to the right per-type translator. + + During streaming these act as safety-net closers (raw events already + streamed the content); for non-streaming runs they emit the item's + full AG-UI sequence. + """ + if isinstance(item, MessageOutputItem): + return self.translate_message_output_item(item) + if isinstance(item, HandoffCallItem): + return self.translate_handoff_call_item(item) + if isinstance(item, HandoffOutputItem): + return self.translate_handoff_output_item(item) + if isinstance(item, ToolCallItem): + return self.translate_tool_call_item(item) + if isinstance(item, ToolCallOutputItem): + return self.translate_tool_call_output_item(item) + if isinstance(item, ReasoningItem): + return self.translate_reasoning_item(item) + if isinstance(item, MCPApprovalRequestItem): + return self.translate_mcp_approval_request_item(item) + if isinstance(item, MCPListToolsItem): + return self.translate_mcp_list_tools_item(item) + if isinstance(item, MCPApprovalResponseItem): + return self.translate_mcp_approval_response_item(item) + logger.debug("Unknown SDK run item type: %s", type(item).__name__) + return [] + + def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEvent]: + """Assistant message commit → close its window, or emit the full triplet. + + Streamed: the raw deltas already carried the text — just close. + Non-streamed: emit ``TEXT_MESSAGE_START/CONTENT/END`` from the finished + item, using the SDK's own :class:`ItemHelpers` extractors (text first, + refusal as fallback — both are user-visible). + """ + raw = item.raw_item + raw_id = read_attr(raw, "id") + action, key = self._reconcile(raw_id, self._open_texts, self._closed_text_ids) + if action == "close": + return self._close_text(key) + if action == "skip": + return [] + text = ItemHelpers.extract_text(raw) or ItemHelpers.extract_refusal(raw) or "" + message_id = self._resolve_id(raw_id, new_message_id) + events = self._open_text(message_id, message_id) + if text: + events.append( + TextMessageContentEvent( + type=EventType.TEXT_MESSAGE_CONTENT, + message_id=message_id, + delta=text, + ) + ) + events.extend(self._close_text(message_id)) + return events + def translate_tool_call_item(self, item: ToolCallItem) -> list[BaseEvent]: + """Tool call commit → close its window, or emit ``START/[ARGS]/END``. + + Reconciled by ``call_id`` (present and real regardless of backend), + using the SDK's built-in :attr:`ToolCallItem.call_id` / + :attr:`ToolCallItem.tool_name` properties where available — + ``getattr`` fallbacks because :class:`HandoffCallItem` routes through + here too and lacks them. + """ + raw = item.raw_item + call_id = ( + getattr(item, "call_id", None) + or read_attr(raw, "call_id") + or self._resolve_id(read_attr(raw, "id"), new_tool_call_id) + ) + for key, open_call_id in self._open_tool_calls.items(): + if open_call_id == call_id: + return self._close_tool_call(key) + if call_id in self._seen_call_ids: + return [] + name = ( + getattr(item, "tool_name", None) + or read_attr(raw, "name") + or read_attr(raw, "type") + or "" + ) + arguments = read_attr(raw, "arguments") or "" + events = self._open_tool_call(call_id, call_id, name) + if arguments: + events.append( + ToolCallArgsEvent( + type=EventType.TOOL_CALL_ARGS, + tool_call_id=call_id, + delta=arguments, + ) + ) + events.extend(self._close_tool_call(call_id)) + return events + + def translate_tool_call_output_item(self, item: ToolCallOutputItem) -> list[BaseEvent]: + """Tool result → ``TOOL_CALL_RESULT``.""" + call_id = item.call_id + if not call_id: + logger.debug("Tool output without call_id; skipping TOOL_CALL_RESULT") + return [] + return [ + ToolCallResultEvent( + type=EventType.TOOL_CALL_RESULT, + message_id=new_tool_result_id(), + tool_call_id=call_id, + content=coerce_to_str(item.output), + ) + ] + + def translate_reasoning_item(self, item: ReasoningItem) -> list[BaseEvent]: + """Reasoning commit → close its phase, or emit the full sequence. + + Non-streamed emission walks the finished ``ResponseReasoningItem``: + one ``REASONING_MESSAGE_*`` window per summary/content entry, then + ``REASONING_ENCRYPTED_VALUE`` (when present), then ``REASONING_END``. + """ + raw = item.raw_item + raw_id = read_attr(raw, "id") + action, key = self._reconcile(raw_id, self._open_reasonings, self._closed_reasoning_ids) + if action == "close": + events = self._emit_encrypted_value(key, raw) + events.extend(self._close_reasoning(key)) + return events + if action == "skip": + return [] + phase_id = self._resolve_id(raw_id, new_message_id) + events = self._open_reasoning(phase_id, phase_id) + parts = [read_attr(entry, "text") for entry in (read_attr(raw, "summary") or [])] + parts += [read_attr(entry, "text") for entry in (read_attr(raw, "content") or [])] + for text in parts: + if not text: + continue + events.extend(self._open_reasoning_part(phase_id)) + events.append( + ReasoningMessageContentEvent( + type=EventType.REASONING_MESSAGE_CONTENT, + message_id=self._open_reasoning_parts[phase_id], + delta=text, + ) + ) + events.extend(self._close_reasoning_part(phase_id)) + events.extend(self._emit_encrypted_value(phase_id, raw)) + events.extend(self._close_reasoning(phase_id)) + return events + + def translate_handoff_call_item(self, item: HandoffCallItem) -> list[BaseEvent]: + """Handoff request → surfaced as a tool call (it *is* a function call). + + The same underlying item also arrives through the raw-event path, so + the call-id reconciliation in :meth:`translate_tool_call_item` + prevents a duplicate ``TOOL_CALL_START``. + """ + return self.translate_tool_call_item(item) # type: ignore[arg-type] + + def translate_handoff_output_item(self, item: HandoffOutputItem) -> list[BaseEvent]: + """Handoff completion → ``TOOL_CALL_RESULT``.""" + raw = item.raw_item + call_id = read_attr(raw, "call_id") + if not call_id: + logger.debug("Handoff output without call_id; skipping TOOL_CALL_RESULT") + return [] + target = read_attr(item.target_agent, "name") or "" + output = read_attr(raw, "output") or f"Handed off to {target}" + return [ + ToolCallResultEvent( + type=EventType.TOOL_CALL_RESULT, + message_id=new_tool_result_id(), + tool_call_id=call_id, + content=coerce_to_str(output), + ) + ] + + def translate_mcp_approval_request_item( + self, + item: MCPApprovalRequestItem, + ) -> list[BaseEvent]: + """MCP approval request → ``CUSTOM`` event (``name="mcp_approval_request"``). + + AG-UI has no native approval shape yet; forwarding the raw request + lets a frontend implement approval UI without protocol changes. + """ + raw = item.raw_item + value = raw.model_dump() if hasattr(raw, "model_dump") else raw + return [ + CustomEvent( + type=EventType.CUSTOM, + name="mcp_approval_request", + value=value, + ) + ] + + def translate_mcp_list_tools_item(self, item: MCPListToolsItem) -> list[BaseEvent]: + """MCP tool listing → dropped (server-side bookkeeping). Overridable.""" + logger.debug("Dropping MCPListToolsItem id=%s", read_attr(item.raw_item, "id")) + return [] + + def translate_mcp_approval_response_item( + self, + item: MCPApprovalResponseItem, + ) -> list[BaseEvent]: + """MCP approval response → dropped (echo of client input). Overridable.""" + logger.debug("Dropping MCPApprovalResponseItem") + return [] + + # ───────────────────────────────────────────────────────────────────── + # TIER 4 — Internal helpers: id handling + # ───────────────────────────────────────────────────────────────────── + + @staticmethod + def _is_real_id(item_id: Any) -> bool: + """True when the id is usable on the wire (not empty, not a placeholder). + + Some model backends stamp every item with the SDK's + ``FAKE_RESPONSES_ID`` sentinel — sharing it across AG-UI events would + collide every message of the run. Checked against the SDK's own + constant, so it's a no-op (always real) on native OpenAI and correct + for any other backend without a provider-specific branch. + """ + return bool(item_id) and item_id != FAKE_RESPONSES_ID + + @classmethod + def _resolve_id(cls, item_id: Any, generate: Any) -> str: + """Return the id if real, else a freshly generated one.""" + return item_id if cls._is_real_id(item_id) else generate() + + @classmethod + def _window_key(cls, item_id: Any, output_index: Any) -> str: + """Stable window key for correlating raw events of one output item. + + Real item ids win; placeholder ids fall back to the stream position + (``output_index``), which the Responses API guarantees is unique per + item within a response. + """ + if cls._is_real_id(item_id): + return item_id + return f"__idx_{output_index}" + + def _reconcile( + self, + raw_id: Any, + open_windows: dict[str, str], + closed_ids: list[str], + ) -> tuple[str, str | None]: + """Match a run-item commit against streamed window state. + + Returns ``("close", key)`` when a streamed window is still open, + ``("skip", None)`` when the item was already fully streamed and + closed, or ``("new", None)`` when nothing was streamed (non-streaming + run) and the item must be emitted whole. + + With a real id the match is exact. With a placeholder id run items + arrive in stream order, so the oldest open window (or one queued + closed id) is consumed instead. + """ + if self._is_real_id(raw_id): + if raw_id in open_windows: + return ("close", raw_id) + if raw_id in closed_ids: + return ("skip", None) + return ("new", None) + if open_windows: + return ("close", next(iter(open_windows))) + if closed_ids: + closed_ids.pop(0) + return ("skip", None) + return ("new", None) + + # ───────────────────────────────────────────────────────────────────── + # TIER 4 — Internal helpers: window management + # ───────────────────────────────────────────────────────────────────── + + def _open_text(self, key: str, message_id: str) -> list[BaseEvent]: + if key in self._open_texts: + return [] + # The model has moved on to producing output — any reasoning phase is + # over, even if the provider's reasoning done-event is still in flight + # (some backends deliver it late). + events = self._close_all_reasonings() + self._open_texts[key] = message_id + events.append( + TextMessageStartEvent( + type=EventType.TEXT_MESSAGE_START, + message_id=message_id, + role="assistant", + ) + ) + return events + + def _emit_text_content(self, key: str, delta: str) -> list[BaseEvent]: + if not delta: + return [] + events: list[BaseEvent] = [] + if key not in self._open_texts: + events.extend(self._open_text(key, new_message_id())) + events.append( + TextMessageContentEvent( + type=EventType.TEXT_MESSAGE_CONTENT, + message_id=self._open_texts[key], + delta=delta, + ) + ) + return events + + def _close_text(self, key: str) -> list[BaseEvent]: + message_id = self._open_texts.pop(key, None) + if message_id is None: + return [] + self._closed_text_ids.append(message_id) + return [ + TextMessageEndEvent( + type=EventType.TEXT_MESSAGE_END, + message_id=message_id, + ) + ] + + def _open_tool_call(self, key: str, call_id: str, name: str) -> list[BaseEvent]: + if key in self._open_tool_calls: + return [] + events = self._close_all_reasonings() # see _open_text + self._open_tool_calls[key] = call_id + self._seen_call_ids.add(call_id) + events.append( + ToolCallStartEvent( + type=EventType.TOOL_CALL_START, + tool_call_id=call_id, + tool_call_name=name, + ) + ) + return events + + def _close_tool_call(self, key: str) -> list[BaseEvent]: + call_id = self._open_tool_calls.pop(key, None) + if call_id is None: + return [] + return [ + ToolCallEndEvent( + type=EventType.TOOL_CALL_END, + tool_call_id=call_id, + ) + ] + + def _open_reasoning(self, key: str, phase_id: str) -> list[BaseEvent]: + if key in self._open_reasonings: + return [] + self._open_reasonings[key] = phase_id + self._reasoning_phase_ids[key] = phase_id + return [ + ReasoningStartEvent( + type=EventType.REASONING_START, + message_id=phase_id, + ) + ] + + def _open_reasoning_part(self, key: str) -> list[BaseEvent]: + if key in self._open_reasoning_parts: + return [] + seq = self._reasoning_part_seq.get(key, 0) + self._reasoning_part_seq[key] = seq + 1 + # First part reuses the phase id (round-trip friendly); later parts + # get a stable derived suffix. + phase_id = self._open_reasonings.get(key, key) + message_id = phase_id if seq == 0 else f"{phase_id}/{seq}" + self._open_reasoning_parts[key] = message_id + return [ + ReasoningMessageStartEvent( + type=EventType.REASONING_MESSAGE_START, + message_id=message_id, + role="reasoning", + ) + ] + + def _close_reasoning_part(self, key: str) -> list[BaseEvent]: + message_id = self._open_reasoning_parts.pop(key, None) + if message_id is None: + return [] + return [ + ReasoningMessageEndEvent( + type=EventType.REASONING_MESSAGE_END, + message_id=message_id, + ) + ] + + def _close_reasoning(self, key: str) -> list[BaseEvent]: + phase_id = self._open_reasonings.pop(key, None) + if phase_id is None: + return [] + events = self._close_reasoning_part(key) + self._closed_reasoning_ids.append(phase_id) + events.append( + ReasoningEndEvent( + type=EventType.REASONING_END, + message_id=phase_id, + ) + ) + return events + + def _close_all_reasonings(self) -> list[BaseEvent]: + events: list[BaseEvent] = [] + for key in list(self._open_reasonings): + events.extend(self._close_reasoning(key)) + return events + + def _emit_encrypted_value(self, key: str, item: Any) -> list[BaseEvent]: + """Emit ``REASONING_ENCRYPTED_VALUE`` once per reasoning item, if present.""" + encrypted = read_attr(item, "encrypted_content") + if not encrypted or key in self._emitted_encrypted_keys: + return [] + self._emitted_encrypted_keys.add(key) + entity_id = self._reasoning_phase_ids.get(key, key) + return [ + ReasoningEncryptedValueEvent( + type=EventType.REASONING_ENCRYPTED_VALUE, + subtype="message", + entity_id=entity_id, + encrypted_value=encrypted, + ) + ] + + +__all__ = ["SDKToAGUITranslator"] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/stream_types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/stream_types.py new file mode 100644 index 0000000000..3e64ec5e33 --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/stream_types.py @@ -0,0 +1,84 @@ +""" +Wire-level discriminator values for OpenAI Agents SDK streaming. + +Single home for every ``type`` string the translators dispatch on, grouped by +the layer that produces it. Members are ``(str, Enum)`` (``StrEnum`` needs +Python 3.11; this package supports 3.9+), so they compare equal to the raw +wire strings — dispatch code can test ``kind == RawResponseEventType.TEXT_DELTA`` +against untyped event payloads with no conversion. + +The SDK is the source of truth for these values: + +- ``SDKStreamEventType`` — ``agents.stream_events.StreamEvent`` union +- ``RawResponseEventType`` — ``openai.types.responses`` event ``type`` literals +- ``SDKItemType`` — ``openai.types.responses`` output-item ``type`` + literals (also mirrored by ``agents.items`` run-item wrappers) + +When a new SDK version adds values not listed here, translators degrade +gracefully (debug log + skip) — see design rule 5 in ``CLAUDE.md``. +""" + +from __future__ import annotations + +from enum import Enum + + +class SDKStreamEventType(str, Enum): + """ + Top-level ``StreamEvent.type`` values yielded by ``Runner.run_streamed``. + """ + + RAW_RESPONSE = "raw_response_event" + RUN_ITEM = "run_item_stream_event" + AGENT_UPDATED = "agent_updated_stream_event" + + +class RawResponseEventType(str, Enum): + """ + Raw Responses-API delta ``type`` values the outbound translator consumes. + + Non-semantic bookkeeping kinds (``response.created`` / ``.completed``, + ``content_part.*``, audio) are deliberately absent — they translate to + nothing. + """ + + OUTPUT_ITEM_ADDED = "response.output_item.added" + OUTPUT_ITEM_DONE = "response.output_item.done" + TEXT_DELTA = "response.output_text.delta" + TEXT_DONE = "response.output_text.done" + REFUSAL_DELTA = "response.refusal.delta" + FUNCTION_CALL_ARGUMENTS_DELTA = "response.function_call_arguments.delta" + REASONING_SUMMARY_DELTA = "response.reasoning_summary_text.delta" + REASONING_SUMMARY_PART_DONE = "response.reasoning_summary_part.done" + REASONING_TEXT_DELTA = "response.reasoning_text.delta" + REASONING_TEXT_DONE = "response.reasoning_text.done" + + +class SDKItemType(str, Enum): + """ + Output-item ``type`` values carried by ``output_item.added`` / ``.done``. + """ + + MESSAGE = "message" + FUNCTION_CALL = "function_call" + REASONING = "reasoning" + + +# Hosted (server-side) tool calls surfaced as AG-UI tool calls. The API never +# streams their arguments, so they get START/END only at the raw level; the +# richer run-item layer fills in details when available. +HOSTED_TOOL_CALL_TYPES: frozenset[str] = frozenset( + { + "web_search_call", + "file_search_call", + "code_interpreter_call", + "image_generation_call", + "computer_call", + "local_shell_call", + "shell_call", + "apply_patch_call", + "mcp_call", + "custom_tool_call", + "tool_search_call", + } +) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/types.py new file mode 100644 index 0000000000..153aa83298 --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/types.py @@ -0,0 +1,114 @@ +""" +Public data containers for the translator package. + +Holds typed result objects produced by the translators. Translator *behavior* +lives in :mod:`agui_to_sdk` and :mod:`sdk_to_agui`; this module only describes +the shapes those translators hand back. +""" + +from __future__ import annotations + +from typing import Any + +from agents import FunctionTool, TResponseInputItem +from ag_ui.core import Context +from pydantic import BaseModel, ConfigDict, SkipValidation + + +class TranslatedInput(BaseModel): + """ + SDK-ready bundle produced by translating an AG-UI ``RunAgentInput``. + + The field shape **mirrors** :class:`ag_ui.core.RunAgentInput` field-for- + field — same required/optional pattern — so callers familiar with the + wire format can map between the two without thinking. Two fields are + renamed because their *content* was translated, not just passed through: + + * ``messages`` → ``input_items`` — now Responses-API input items. + * ``tools`` → ``function_tools`` — now SDK :class:`FunctionTool` proxies. + + Everything else is passed through unchanged so downstream code decides + what to do with raw frontend payloads (e.g. ``context``, ``forwarded_props``). + + Required fields (must always be provided): + thread_id, run_id, input_items, function_tools, + state, context, forwarded_props. + + Optional fields (default to ``None``): + parent_run_id, resume. + """ + + # ── Identity ───────────────────────────────────────────────────────── + thread_id: str + """ + Session key for this conversation. Same value across multi-turn runs and + used by the session manager for HITL resume. + """ + + run_id: str + """ + Unique id for this specific run. Tagged onto ``RUN_STARTED`` / + ``RUN_FINISHED`` events emitted back to the client. + """ + + parent_run_id: str | None = None + """ + Optional parent run id — set when this run was triggered by another run + (e.g. a nested or branched agent invocation). ``None`` for top-level runs. + """ + + # ── Translated payload (the actual work the translator does) ──────── + messages: list[TResponseInputItem] + """ + Responses-API input list. Feed straight into + ``Runner.run_streamed(input=...)``. Built from + :attr:`ag_ui.core.RunAgentInput.messages`. + + Validation is skipped to avoid Pydantic forward-ref resolution issues + with the agents SDK's internal types. + """ + + # tools: SkipValidation[list[FunctionTool]] + """ + SDK :class:`FunctionTool` proxies for the AG-UI client tools. Merge with + the SDK agent's static tools before running. Built from + :attr:`ag_ui.core.RunAgentInput.tools`. + + Validation is skipped to avoid Pydantic forward-ref resolution issues + with the agents SDK's internal types. + """ + + # ── Passthroughs (raw from the wire) ──────────────────────────────── + state: Any + """ + The AG-UI user state as sent by the client. Typed ``Any`` because AG-UI + itself doesn't constrain its shape. + """ + + context: list[Context] + """ + Ambient context items (CopilotKit's ``useCopilotReadable`` etc.) — each + a ``{description, value}`` pair. Not auto-folded anywhere; use + :meth:`AGUIToSDKTranslator.translate_context` to render for the model. + """ + + forwarded_props: Any + """ + Catch-all client-supplied props (model overrides, temperature, debug + flags, anything the client doesn't have a typed field for). + """ + + resume: list[Any] | None = None + """ + HITL resume entries from the client — each ``{interruptId, status, + payload?}``. Only present when continuing from a previous interrupt. + + The Python AG-UI SDK does not expose this field yet; the translator reads + it defensively so the bundle is forward-compatible with the wire protocol. + """ + + # FunctionTool is not a Pydantic model, so we need to opt in. + model_config = ConfigDict(arbitrary_types_allowed=True) + + +__all__ = ["TranslatedInput"] diff --git a/integrations/openai-agents/python/tests/test_stream_types_drift.py b/integrations/openai-agents/python/tests/test_stream_types_drift.py new file mode 100644 index 0000000000..999f55950a --- /dev/null +++ b/integrations/openai-agents/python/tests/test_stream_types_drift.py @@ -0,0 +1,143 @@ +""" +Drift guard: our wire strings must match the installed SDK's own types. + +``stream_types.py`` hardcodes the discriminator strings the translators +dispatch on. The SDK declares the same strings as ``Literal[...]`` annotations +on its event/item classes. Nothing compares the two at runtime — if the SDK +renamed a wire value, dispatch would silently stop matching (graceful +degradation swallows unknowns). These tests make that drift a loud CI failure +instead: bump ``openai-agents`` / ``openai``, run pytest, and any renamed or +newly added wire type shows up as an assertion diff pointing at the exact +enum member to update by hand. +""" + +from __future__ import annotations + +import dataclasses +from typing import Union, get_args, get_origin + +import pytest +from agents.stream_events import ( + AgentUpdatedStreamEvent, + RawResponsesStreamEvent, + RunItemStreamEvent, +) +from openai.types.responses import ( + ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionToolCall, + ResponseOutputItem, + ResponseOutputItemAddedEvent, + ResponseOutputItemDoneEvent, + ResponseOutputMessage, + ResponseReasoningItem, + ResponseReasoningSummaryPartDoneEvent, + ResponseReasoningSummaryTextDeltaEvent, + ResponseRefusalDeltaEvent, + ResponseTextDeltaEvent, + ResponseTextDoneEvent, +) +from openai.types.responses.response_reasoning_text_delta_event import ( + ResponseReasoningTextDeltaEvent, +) +from openai.types.responses.response_reasoning_text_done_event import ( + ResponseReasoningTextDoneEvent, +) + +from ag_ui_openai_agents.translator import ( + HOSTED_TOOL_CALL_TYPES, + RawResponseEventType, + SDKItemType, + SDKStreamEventType, +) + + +def pydantic_wire_value(model_cls: type) -> str: + """Extract the wire string from a pydantic ``type: Literal['...']`` field.""" + return get_args(model_cls.model_fields["type"].annotation)[0] + + +def dataclass_wire_value(cls: type) -> str: + """Extract the wire string from a dataclass ``type`` field default.""" + for field in dataclasses.fields(cls): + if field.name == "type": + return field.default + raise AssertionError(f"{cls.__name__} has no 'type' field") + + +def output_item_union_members() -> tuple[type, ...]: + """Unwrap ``ResponseOutputItem`` (``Annotated[Union[...], meta]``).""" + inner = get_args(ResponseOutputItem)[0] + assert get_origin(inner) is Union + return get_args(inner) + + +@pytest.mark.parametrize( + ("ours", "sdk_cls"), + [ + (SDKStreamEventType.RAW_RESPONSE, RawResponsesStreamEvent), + (SDKStreamEventType.RUN_ITEM, RunItemStreamEvent), + (SDKStreamEventType.AGENT_UPDATED, AgentUpdatedStreamEvent), + ], +) +def test_stream_event_types_match_sdk(ours: SDKStreamEventType, sdk_cls: type) -> None: + assert ours == dataclass_wire_value(sdk_cls) + + +@pytest.mark.parametrize( + ("ours", "sdk_cls"), + [ + (RawResponseEventType.OUTPUT_ITEM_ADDED, ResponseOutputItemAddedEvent), + (RawResponseEventType.OUTPUT_ITEM_DONE, ResponseOutputItemDoneEvent), + (RawResponseEventType.TEXT_DELTA, ResponseTextDeltaEvent), + (RawResponseEventType.TEXT_DONE, ResponseTextDoneEvent), + (RawResponseEventType.REFUSAL_DELTA, ResponseRefusalDeltaEvent), + ( + RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + ResponseFunctionCallArgumentsDeltaEvent, + ), + ( + RawResponseEventType.REASONING_SUMMARY_DELTA, + ResponseReasoningSummaryTextDeltaEvent, + ), + ( + RawResponseEventType.REASONING_SUMMARY_PART_DONE, + ResponseReasoningSummaryPartDoneEvent, + ), + (RawResponseEventType.REASONING_TEXT_DELTA, ResponseReasoningTextDeltaEvent), + (RawResponseEventType.REASONING_TEXT_DONE, ResponseReasoningTextDoneEvent), + ], +) +def test_raw_response_event_types_match_sdk( + ours: RawResponseEventType, sdk_cls: type +) -> None: + assert ours == pydantic_wire_value(sdk_cls) + + +@pytest.mark.parametrize( + ("ours", "sdk_cls"), + [ + (SDKItemType.MESSAGE, ResponseOutputMessage), + (SDKItemType.FUNCTION_CALL, ResponseFunctionToolCall), + (SDKItemType.REASONING, ResponseReasoningItem), + ], +) +def test_item_types_match_sdk(ours: SDKItemType, sdk_cls: type) -> None: + assert ours == pydantic_wire_value(sdk_cls) + + +def test_every_sdk_tool_call_item_type_is_known() -> None: + """ + Catch the SDK *adding* hosted tool kinds, not just renaming them. + + Every ``*_call`` member of the ``ResponseOutputItem`` union must be either + ``function_call`` or listed in ``HOSTED_TOOL_CALL_TYPES`` — otherwise a new + hosted tool would silently translate to nothing. + """ + sdk_call_types = { + pydantic_wire_value(member) + for member in output_item_union_members() + if pydantic_wire_value(member).endswith("_call") + } + known = HOSTED_TOOL_CALL_TYPES | {SDKItemType.FUNCTION_CALL.value} + unknown = sdk_call_types - known + assert not unknown, f"SDK added tool-call item types we don't map: {unknown}" diff --git a/integrations/openai-agents/python/uv.lock b/integrations/openai-agents/python/uv.lock index 11840d2f62..5f4d004340 100644 --- a/integrations/openai-agents/python/uv.lock +++ b/integrations/openai-agents/python/uv.lock @@ -12,18 +12,29 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "ag-ui-protocol" }, + { name = "jsonpatch" }, { name = "openai-agents", version = "0.8.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "openai-agents", version = "0.17.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pydantic" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + [package.metadata] requires-dist = [ { name = "ag-ui-protocol", specifier = ">=0.1.18" }, + { name = "jsonpatch", specifier = ">=1.33" }, { name = "openai-agents", specifier = ">=0.8.4" }, { name = "pydantic", specifier = ">=2.13.4" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0" }] + [[package]] name = "ag-ui-protocol" version = "0.1.18" @@ -491,6 +502,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jiter" version = "0.14.0" @@ -607,6 +642,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, ] +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jsonpointer", version = "3.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -721,6 +793,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/71/5ba9afa4b6a0d250bfdbdd1cf9a5255ae131a7f82915c634ac50c0633c3a/openai_agents-0.17.2-py3-none-any.whl", hash = "sha256:1b3560c1690bcee635a487f77ebfb8b4fb2dd52a653e045a86e51974ab87faf3", size = 838225, upload-time = "2026-05-12T03:14:55.149Z" }, ] +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -889,6 +979,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyjwt" version = "2.12.1" @@ -906,6 +1005,48 @@ crypto = [ { name = "cryptography", marker = "python_full_version >= '3.10'" }, ] +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1156,6 +1297,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "tqdm" version = "4.67.3" From cb02b166ea4d7d933a8730bc04cb8d033107a5a7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 5 Jul 2026 23:44:08 +0200 Subject: [PATCH 03/94] =?UTF-8?q?feat(translator):=20standardize=20id=20de?= =?UTF-8?q?rivation=20=E2=80=94=20tool=20results=20and=20reasoning=20parts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change tool_result_id from random `toolresult_` to deterministic `{call_id}-result`, linking result message visually and functionally to its tool call. Matches claude-agent-sdk pattern. - Change reasoning part id separator from `/` to `-` (e.g., `rs_abc-1` not `rs_abc/1`). Hyphen never appears in wire ids, so suffix clearly marks our derivations. - new_tool_result_id(call_id) now takes call_id param; determinism aids dedup/replay safety. - Reasoning fallback generation via new_reasoning_id() was already in place (rs_ prefix). All generated id shapes now mimic their native OpenAI counterparts; hyphen-suffixed ids are transparently ours. Pass-through chain unchanged (call_id → fc_ → generated). --- .../ag_ui_openai_agents/translator/helpers.py | 16 +++++++++++++--- .../translator/sdk_to_agui.py | 16 +++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py index 07cfe4017d..9f4e7780c1 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py @@ -23,14 +23,24 @@ def new_message_id() -> str: return f"msg_{uuid.uuid4().hex}" +def new_reasoning_id() -> str: + """Generate a fresh reasoning id (``rs_``, matching the wire prefix).""" + return f"rs_{uuid.uuid4().hex}" + + def new_tool_call_id() -> str: """Generate a fresh tool call id (``call_``).""" return f"call_{uuid.uuid4().hex}" -def new_tool_result_id() -> str: - """Generate a fresh tool result id (``toolresult_``).""" - return f"toolresult_{uuid.uuid4().hex}" +def new_tool_result_id(call_id: str) -> str: + """Derive the tool result message id (``-result``). + + Deterministic — the wire has no id on ``function_call_output``, so the + result id is derived from the ``call_id`` it answers. Hyphen never + appears in wire ids, marking the suffix as ours. + """ + return f"{call_id}-result" # --------------------------------------------------------------------------- diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py index a38d9238a5..543537e071 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py @@ -103,6 +103,7 @@ from .helpers import ( coerce_to_str, new_message_id, + new_reasoning_id, new_tool_call_id, new_tool_result_id, read_attr, @@ -300,7 +301,7 @@ def translate_output_item_added(self, data: Any) -> list[BaseEvent]: name = read_attr(item, "name") or "" return self._open_tool_call(key, call_id, name) if item_type == SDKItemType.REASONING: - return self._open_reasoning(key, self._resolve_id(item_id, new_message_id)) + return self._open_reasoning(key, self._resolve_id(item_id, new_reasoning_id)) if item_type in HOSTED_TOOL_CALL_TYPES: call_id = self._resolve_id(item_id, new_tool_call_id) return self._open_tool_call(key, call_id, item_type) @@ -386,7 +387,7 @@ def translate_reasoning_delta(self, data: Any) -> list[BaseEvent]: key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) events: list[BaseEvent] = [] if key not in self._open_reasonings: - events.extend(self._open_reasoning(key, new_message_id())) + events.extend(self._open_reasoning(key, new_reasoning_id())) if key not in self._open_reasoning_parts: events.extend(self._open_reasoning_part(key)) events.append( @@ -511,7 +512,7 @@ def translate_tool_call_output_item(self, item: ToolCallOutputItem) -> list[Base return [ ToolCallResultEvent( type=EventType.TOOL_CALL_RESULT, - message_id=new_tool_result_id(), + message_id=new_tool_result_id(call_id), tool_call_id=call_id, content=coerce_to_str(item.output), ) @@ -533,7 +534,7 @@ def translate_reasoning_item(self, item: ReasoningItem) -> list[BaseEvent]: return events if action == "skip": return [] - phase_id = self._resolve_id(raw_id, new_message_id) + phase_id = self._resolve_id(raw_id, new_reasoning_id) events = self._open_reasoning(phase_id, phase_id) parts = [read_attr(entry, "text") for entry in (read_attr(raw, "summary") or [])] parts += [read_attr(entry, "text") for entry in (read_attr(raw, "content") or [])] @@ -574,7 +575,7 @@ def translate_handoff_output_item(self, item: HandoffOutputItem) -> list[BaseEve return [ ToolCallResultEvent( type=EventType.TOOL_CALL_RESULT, - message_id=new_tool_result_id(), + message_id=new_tool_result_id(call_id), tool_call_id=call_id, content=coerce_to_str(output), ) @@ -767,9 +768,10 @@ def _open_reasoning_part(self, key: str) -> list[BaseEvent]: seq = self._reasoning_part_seq.get(key, 0) self._reasoning_part_seq[key] = seq + 1 # First part reuses the phase id (round-trip friendly); later parts - # get a stable derived suffix. + # get a stable derived suffix. Hyphen never appears in wire ids, so + # the suffix is unambiguously ours. phase_id = self._open_reasonings.get(key, key) - message_id = phase_id if seq == 0 else f"{phase_id}/{seq}" + message_id = phase_id if seq == 0 else f"{phase_id}-{seq}" self._open_reasoning_parts[key] = message_id return [ ReasoningMessageStartEvent( From 7c41a2ae957aaf6038d29c9494407547076cf89f Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 6 Jul 2026 20:42:08 +0200 Subject: [PATCH 04/94] feat(dependencies): remove jsonpatch from project dependencies --- .gitignore | 3 ++ .../openai-agents/python/pyproject.toml | 1 - integrations/openai-agents/python/uv.lock | 39 ------------------- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index dbb12bb6cc..252ee07879 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ node_modules # actionlint linter binary (dev tool, do not commit) /actionlint + +# Dev +.playwright-mcp/ diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml index b47cc0dfc7..3cdd02d2d7 100644 --- a/integrations/openai-agents/python/pyproject.toml +++ b/integrations/openai-agents/python/pyproject.toml @@ -11,7 +11,6 @@ dependencies = [ "ag-ui-protocol>=0.1.18", "openai-agents>=0.8.4", "pydantic>=2.13.4", - "jsonpatch>=1.33", ] [dependency-groups] diff --git a/integrations/openai-agents/python/uv.lock b/integrations/openai-agents/python/uv.lock index 5f4d004340..3bc1e7e05f 100644 --- a/integrations/openai-agents/python/uv.lock +++ b/integrations/openai-agents/python/uv.lock @@ -12,7 +12,6 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "ag-ui-protocol" }, - { name = "jsonpatch" }, { name = "openai-agents", version = "0.8.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "openai-agents", version = "0.17.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pydantic" }, @@ -27,7 +26,6 @@ dev = [ [package.metadata] requires-dist = [ { name = "ag-ui-protocol", specifier = ">=0.1.18" }, - { name = "jsonpatch", specifier = ">=1.33" }, { name = "openai-agents", specifier = ">=0.8.4" }, { name = "pydantic", specifier = ">=2.13.4" }, ] @@ -642,43 +640,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, ] -[[package]] -name = "jsonpatch" -version = "1.33" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpointer", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jsonpointer", version = "3.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, -] - -[[package]] -name = "jsonpointer" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, -] - -[[package]] -name = "jsonpointer" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, -] - [[package]] name = "jsonschema" version = "4.26.0" From 3253d2b0ebd3e1365ffbbfb7ab0b6ea41606a3d1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 6 Jul 2026 20:48:45 +0200 Subject: [PATCH 05/94] feat(translator): restructure API with public interfaces and engine subpackage - Create public interfaces: AGUITranslator (streamed) and AGUINonStreamingTranslator (sync/async) - Move per-direction translation logic into engine/ subpackage for better separation of concerns - Flatten public module structure at package root to align with sibling integrations - Maintain backward compatibility: translator/ subpackage imports resolve via translator.py - Update README to serve as integration usage documentation This restructuring improves maintainability by isolating implementation details while keeping the public API simple and discoverable. --- integrations/openai-agents/python/README.md | 230 +++++++++++++++++- .../src/ag_ui_openai_agents/__init__.py | 31 ++- .../{translator => engine}/__init__.py | 28 +-- .../{translator => engine}/agui_to_sdk.py | 0 .../{translator => engine}/helpers.py | 57 ----- .../{translator => engine}/sdk_to_agui.py | 0 .../{translator => engine}/stream_types.py | 0 .../{translator => engine}/types.py | 13 +- .../non_streaming_translator.py | 80 ++++++ .../src/ag_ui_openai_agents/translator.py | 88 +++++++ .../python/tests/test_stream_types_drift.py | 2 +- .../python/tests/test_translators.py | 122 ++++++++++ 12 files changed, 560 insertions(+), 91 deletions(-) rename integrations/openai-agents/python/src/ag_ui_openai_agents/{translator => engine}/__init__.py (52%) rename integrations/openai-agents/python/src/ag_ui_openai_agents/{translator => engine}/agui_to_sdk.py (100%) rename integrations/openai-agents/python/src/ag_ui_openai_agents/{translator => engine}/helpers.py (53%) rename integrations/openai-agents/python/src/ag_ui_openai_agents/{translator => engine}/sdk_to_agui.py (100%) rename integrations/openai-agents/python/src/ag_ui_openai_agents/{translator => engine}/stream_types.py (100%) rename integrations/openai-agents/python/src/ag_ui_openai_agents/{translator => engine}/types.py (89%) create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py create mode 100644 integrations/openai-agents/python/tests/test_translators.py diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 79d41ec84c..d89954357d 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -1,16 +1,236 @@ # AG-UI × OpenAI Agents SDK -Integrates [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) -with [AG-UI Protocol](https://github.com/ag-ui-protocol/ag-ui). Build your -agent with the OpenAI SDK as usual, then stream its execution as AG-UI events. +Integrates the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) +with the [AG-UI Protocol](https://github.com/ag-ui-protocol/ag-ui). Build your +agent with the OpenAI SDK as usual, then translate its execution into AG-UI +events any AG-UI client (e.g. CopilotKit) can render live. + +The integration is a pair of **translators** — it converts data in both +directions and stays out of your way. You own the agent, the run loop, and +the transport; the translators only map shapes: + +``` +AG-UI RunAgentInput ──to_sdk()──▶ SDK input items + tools +SDK stream events ──to_agui()─▶ AG-UI BaseEvents +``` ## Install ```bash pip install ag-ui-openai-agent-sdk +``` + +For local development this package uses [uv](https://docs.astral.sh/uv/): + +```bash uv sync ``` +## Quick start + +A complete SSE endpoint — agent, FastAPI, and the streaming translator: + +```python +from agents import Agent, Runner +from ag_ui.core import ( + EventType, RunAgentInput, + RunErrorEvent, RunFinishedEvent, RunStartedEvent, +) +from fastapi import FastAPI +from fastapi.responses import StreamingResponse + +from ag_ui_openai_agents import AGUITranslator + +agent = Agent(name="assistant", instructions="Be concise.") +translator = AGUITranslator() # stateless — one instance serves every request +app = FastAPI() + + +@app.post("/") +async def run(body: RunAgentInput) -> StreamingResponse: + return StreamingResponse(_stream(body), media_type="text/event-stream") + + +async def _stream(body: RunAgentInput): + # 1 — AG-UI input → SDK-ready bundle (messages, tool proxies, passthroughs) + bundle = translator.to_sdk(body) + + # Client-declared (frontend) tools arrive on the wire — merge per request. + run_agent = agent + if bundle.tools: + run_agent = agent.clone(tools=[*agent.tools, *bundle.tools]) + + # 2 — lifecycle events are yours, not the translator's + yield _sse(RunStartedEvent( + type=EventType.RUN_STARTED, thread_id=body.thread_id, run_id=body.run_id, + )) + + try: + # 3 — run the SDK agent; stream translated AG-UI events + result = Runner.run_streamed(run_agent, input=bundle.messages) + async for event in translator.to_agui(result.stream_events()): + yield _sse(event) + except Exception: + yield _sse(RunErrorEvent(type=EventType.RUN_ERROR, message="Agent run failed.")) + return + + yield _sse(RunFinishedEvent( + type=EventType.RUN_FINISHED, thread_id=body.thread_id, run_id=body.run_id, + )) + + +def _sse(event) -> bytes: + return f"data: {event.model_dump_json(by_alias=True, exclude_none=True)}\n\n".encode() +``` + +Test it: + +```bash +curl -N -X POST http://localhost:8000 \ + -H 'Content-Type: application/json' \ + -d '{ + "thread_id": "t1", "run_id": "r1", + "messages": [{"id":"m1","role":"user","content":"Say hi in one sentence."}], + "tools": [], "state": {}, "context": [], "forwarded_props": null + }' +``` + +Expected: `RUN_STARTED → TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT (×N) → +TEXT_MESSAGE_END → RUN_FINISHED`. + +A full multi-demo server (chat, backend tools, human-in-the-loop, handoffs, +orchestrator) lives in [`examples/`](examples/). + +## Public API + +Two facade translators, **two methods each** — everything else is theirs to +orchestrate: + +| Class | Pairs with | `to_agui(...)` returns | +|---|---|---| +| `AGUITranslator` (main) | `Runner.run_streamed` | async iterator of AG-UI events, live | +| `AGUINonStreamingTranslator` | `Runner.run` / `run_sync` | `list[BaseEvent]`, one shot | + +Both are stateless and reusable — each `to_agui` call internally creates the +fresh per-run engine it needs. Create one instance and share it. + +### Streaming (the default) + +AG-UI is an ordered event stream by design, so streaming is the primary mode: + +```python +translator = AGUITranslator() +bundle = translator.to_sdk(run_input) +result = Runner.run_streamed(agent, input=bundle.messages) +async for event in translator.to_agui(result.stream_events()): + ... # AG-UI BaseEvent, ready to encode +``` + +`to_agui` folds all window bookkeeping in: any still-open text / tool-call / +reasoning window is closed automatically when the stream ends. + +### Non-streaming + +Same valid AG-UI event sequence, produced in one burst after the run +finishes (no token-level deltas): + +```python +translator = AGUINonStreamingTranslator() +bundle = translator.to_sdk(run_input) +result = await Runner.run(agent, input=bundle.messages) +events = translator.to_agui(result) # accepts RunResult or list[RunItem] +``` + +### What `to_sdk` gives you + +`TranslatedInput` mirrors `RunAgentInput` field for field: + +| AG-UI field | Lands in | +|---|---| +| `messages` | `bundle.messages` — Responses-API input items for `Runner.run*` | +| `tools` | `bundle.tools` — SDK `FunctionTool` proxies for client-declared tools; merge with `agent.clone(tools=[*agent.tools, *bundle.tools])` | +| `state`, `context`, `forwarded_props` | passthrough — the library never injects them anywhere; render them into instructions/messages yourself if your app needs the model to see them | +| `thread_id`, `run_id`, `parent_run_id`, `resume` | passthrough | + +### Division of labor + +The translators translate; **your run loop orchestrates**. Lifecycle events +(`RUN_STARTED`/`RUN_FINISHED`/`RUN_ERROR`), `STATE_SNAPSHOT`, +`MESSAGES_SNAPSHOT`, session persistence, and transport (SSE/WebSocket) +stay in your code — see the quick start above for the minimal shape. + +## Advanced: the engine layer + +The facades delegate to two independent, symmetric engine translators in +`ag_ui_openai_agents.engine`: + +- `AGUIToSDKTranslator` — inbound; stateless, tiered per-type methods + (`translate_user_message`, `translate_tool_message`, ...) +- `SDKToAGUITranslator` — outbound; stateful per run (open text/tool/reasoning + windows), per-type methods (`translate_text_delta`, `translate_item`, ...) + +Every per-type method is a public override point. To customize one mapping, +subclass the engine and inject it — the facade and every other mapping stay +untouched: + +```python +from ag_ui_openai_agents import AGUITranslator +from ag_ui_openai_agents.engine import SDKToAGUITranslator + + +class MyOutbound(SDKToAGUITranslator): + def translate_text_delta(self, data): + ... # your variant of one mapping + +translator = AGUITranslator(outbound_cls=MyOutbound) +``` + +## Frontend (client-owned) tools + +Tools declared in `RunAgentInput.tools` belong to the frontend. `to_sdk` +turns them into SDK `FunctionTool` proxies so the model can call them; for +human-in-the-loop flows, end the run the moment such a tool is called by +using the SDK's native mechanism: + +```python +from agents import Agent, StopAtTools + +agent = Agent( + ..., + tool_use_behavior=StopAtTools(stop_at_tool_names=["confirm_changes"]), +) +``` + +The run stops before the proxy body executes; the frontend answers the tool +call and sends the result back as a `tool` message in the next run's +`messages`. See `examples/agents_examples/human_in_the_loop.py`. + +## Supported AG-UI events + +- **Text**: `TEXT_MESSAGE_START` / `CONTENT` / `END` (token-level, refusals included) +- **Tool calls**: `TOOL_CALL_START` / `ARGS` / `END` / `RESULT` (args stream as deltas) +- **Reasoning**: `REASONING_START` / `MESSAGE_*` / `END`, plus + `REASONING_ENCRYPTED_VALUE` for replayable reasoning +- **Hosted tools** (web search, file search, code interpreter, ...): full + `TOOL_CALL_*` sequences +- **Handoffs & agents-as-tools**: translated as tool calls; `STEP_STARTED` / + `STEP_FINISHED` for multi-agent steps +- **MCP approval requests**: `CUSTOM` events + +Lifecycle (`RUN_*`) and snapshot events are emitted by your run loop, not +the translator (see division of labor above). + +## Gotchas + +- **Reasoning replay** (sending reasoning back to OpenAI) only works via + `encrypted_content` — set + `ModelSettings(response_include=["reasoning.encrypted_content"])` on the + run. Plaintext reasoning cannot be re-ingested and is dropped inbound. +- The Responses API has no `tool` role — AG-UI `tool` messages become + `function_call_output` items linked by `call_id`. +- Unknown/unsupported message and event types never crash the translators — + they are dropped with a debug log. + ## Testing ```bash @@ -20,11 +240,11 @@ uv run pytest # run the full suite The suite includes a **drift guard** (`tests/test_stream_types_drift.py`): this package hardcodes the wire `type` strings it dispatches on (in -`translator/stream_types.py`), and the guard asserts each one against the +`engine/stream_types.py`), and the guard asserts each one against the `Literal[...]` annotations of the installed `openai-agents` / `openai` packages. After bumping either dependency, run `uv run pytest` — if a wire type was renamed or a new hosted tool-call item type was added, the guard fails with an assertion diff naming the exact value to update in `stream_types.py`. Unknown types never crash at runtime (the translator degrades gracefully and skips them); the guard exists so drift is caught in -CI instead of silently dropping events. \ No newline at end of file +CI instead of silently dropping events. diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py index 9874b2a731..460343fc10 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py @@ -1,30 +1,35 @@ -"""OpenAI Agents SDK × AG-UI Protocol integration.""" +"""OpenAI Agents SDK × AG-UI Protocol integration. + +Primary API — two-method facades, one per run mode:: + + from ag_ui_openai_agents import AGUITranslator, AGUINonStreamingTranslator + +Advanced (per-mapping overrides) — the engine layer:: + + from ag_ui_openai_agents.engine import AGUIToSDKTranslator, SDKToAGUITranslator +""" from __future__ import annotations -# -# from .agent import OpenAIAgentsAgent -# from .endpoint import add_fastapi_endpoint, create_app -from .translator import ( + +from .engine import ( AGUIToSDKTranslator, ClientToolPending, SDKToAGUITranslator, - StateDiffer, TranslatedInput, ) +from .non_streaming_translator import AGUINonStreamingTranslator +from .translator import AGUITranslator __version__ = "0.1.0" __all__ = [ - # Main agent wrapper - # "OpenAIAgentsAgent", - # # FastAPI wiring - # "add_fastapi_endpoint", - # "create_app", - # Translators (for advanced / manual use) + # Facade translators (primary API — 2 methods each) + "AGUITranslator", + "AGUINonStreamingTranslator", + # Engine translators (advanced / per-mapping overrides) "AGUIToSDKTranslator", "SDKToAGUITranslator", # Types & helpers "TranslatedInput", "ClientToolPending", - "StateDiffer", ] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py similarity index 52% rename from integrations/openai-agents/python/src/ag_ui_openai_agents/translator/__init__.py rename to integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py index 433013b808..0bc8ddda33 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py @@ -1,15 +1,19 @@ """ -Bi-directional translation between AG-UI and the OpenAI Agents SDK. +Engine layer — the per-direction translation logic under the facades. -Two independent classes — one per direction — plus shared helpers and a -typed result container. Import what you need:: +Advanced API: subclass an engine to customize one mapping (design rule 4) +and inject it into a facade via ``inbound_cls`` / ``outbound_cls``. For +normal use import the facades from the package root instead +(:class:`~ag_ui_openai_agents.AGUITranslator`, +:class:`~ag_ui_openai_agents.AGUINonStreamingTranslator`). - from ag_ui_openai_agents.translator import ( - AGUIToSDKTranslator, # inbound: AG-UI primitives → SDK shapes - SDKToAGUITranslator, # outbound: SDK formats → AG-UI primitives - TranslatedInput, # Pydantic bundle returned by translate() - ClientToolPending, # sentinel raised by client-tool proxies - StateDiffer, # JSON Patch helper for STATE_DELTA events +:: + + from ag_ui_openai_agents.engine import ( + AGUIToSDKTranslator, # inbound: AG-UI primitives → SDK shapes + SDKToAGUITranslator, # outbound: SDK formats → AG-UI primitives + TranslatedInput, # Pydantic bundle returned by translate()/to_sdk() + ClientToolPending, # sentinel raised by client-tool proxies ) """ @@ -17,13 +21,11 @@ from .agui_to_sdk import AGUIToSDKTranslator, ClientToolPending from .helpers import ( - StateDiffer, coerce_to_str, new_message_id, new_tool_call_id, new_tool_result_id, read_attr, - snapshot_state, ) from .sdk_to_agui import SDKToAGUITranslator from .stream_types import ( @@ -35,7 +37,7 @@ from .types import TranslatedInput __all__ = [ - # Translators + # Engine translators "AGUIToSDKTranslator", "SDKToAGUITranslator", # Result types @@ -48,8 +50,6 @@ # Sentinels "ClientToolPending", # Shared helpers - "StateDiffer", - "snapshot_state", "new_message_id", "new_tool_call_id", "new_tool_result_id", diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/agui_to_sdk.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py similarity index 100% rename from integrations/openai-agents/python/src/ag_ui_openai_agents/translator/agui_to_sdk.py rename to integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py similarity index 53% rename from integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py rename to integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py index 9f4e7780c1..8f92bd4510 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/helpers.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py @@ -6,13 +6,10 @@ from __future__ import annotations -import copy import json import uuid from typing import Any -import jsonpatch - # --------------------------------------------------------------------------- # ID generation @@ -73,64 +70,10 @@ def coerce_to_str(value: Any) -> str: return str(value) -# --------------------------------------------------------------------------- -# State diffing (JSON Patch RFC 6902) -# --------------------------------------------------------------------------- - -def snapshot_state(state: dict[str, Any]) -> dict[str, Any]: - """Deep-copy ``state`` so later mutations don't pollute the baseline.""" - return copy.deepcopy(state) - - -class StateDiffer: - """Tracks a baseline state and yields JSON Patch ops when it changes. - - Used by the outbound translator (and the agent loop) to surface mutations - that tools/instructions made to ``AGUIContext.state`` as AG-UI - ``STATE_DELTA`` events. - - Lifecycle:: - - differ = StateDiffer(initial=context.state) - # ... after each SDK event ... - ops = differ.diff(context.state) - if ops is not None: - yield StateDeltaEvent(type=..., delta=ops) - """ - - def __init__(self, initial: dict[str, Any]) -> None: - self._baseline: dict[str, Any] = snapshot_state(initial) - - def diff(self, current: dict[str, Any]) -> list[dict[str, Any]] | None: - """Return JSON Patch ops if ``current`` differs from the baseline. - - Returns ``None`` (not ``[]``) when nothing changed — so callers can use - a simple ``if ops is not None`` check before emitting an event. - After a non-empty diff, the baseline advances to ``current``. - """ - patch = jsonpatch.make_patch(self._baseline, current) - ops = list(patch) - if not ops: - return None - self._baseline = snapshot_state(current) - return ops - - @property - def baseline(self) -> dict[str, Any]: - """Return a copy of the current baseline (defensive).""" - return snapshot_state(self._baseline) - - def reset(self, state: dict[str, Any]) -> None: - """Force-reset the baseline to ``state`` without emitting a diff.""" - self._baseline = snapshot_state(state) - - __all__ = [ "new_message_id", "new_tool_call_id", "new_tool_result_id", "read_attr", "coerce_to_str", - "snapshot_state", - "StateDiffer", ] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py similarity index 100% rename from integrations/openai-agents/python/src/ag_ui_openai_agents/translator/sdk_to_agui.py rename to integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/stream_types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py similarity index 100% rename from integrations/openai-agents/python/src/ag_ui_openai_agents/translator/stream_types.py rename to integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py similarity index 89% rename from integrations/openai-agents/python/src/ag_ui_openai_agents/translator/types.py rename to integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index 153aa83298..6e7721a521 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -68,12 +68,17 @@ class TranslatedInput(BaseModel): with the agents SDK's internal types. """ - # tools: SkipValidation[list[FunctionTool]] + tools: SkipValidation[list[FunctionTool]] = [] """ SDK :class:`FunctionTool` proxies for the AG-UI client tools. Merge with the SDK agent's static tools before running. Built from :attr:`ag_ui.core.RunAgentInput.tools`. + Populated by the facade translators' ``to_sdk``; the engine's + ``AGUIToSDKTranslator.translate`` leaves it empty pending the + tools-wiring review fix (call ``translate_tools`` when using the engine + directly). + Validation is skipped to avoid Pydantic forward-ref resolution issues with the agents SDK's internal types. """ @@ -111,4 +116,10 @@ class TranslatedInput(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) +# FunctionTool's dataclass schema references the SDK-internal ``AgentBase`` +# forward ref; import it into scope and rebuild so ``tools`` resolves. +from agents.agent import AgentBase # noqa: E402,F401 + +TranslatedInput.model_rebuild() + __all__ = ["TranslatedInput"] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py new file mode 100644 index 0000000000..63da85d345 --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py @@ -0,0 +1,80 @@ +""" +Non-streaming facade. + +:class:`AGUINonStreamingTranslator` pairs with ``Runner.run`` / +``Runner.run_sync`` and exposes exactly two public methods, one per +direction: + + ``to_sdk(run_input)`` AG-UI ``RunAgentInput`` → SDK-ready bundle + ``to_agui(result)`` finished SDK run → complete AG-UI event list + +All translation logic lives in the engine layer (:mod:`.engine`); this +class only orchestrates it. Stateless and +reusable — the engine instance a call needs is created inside it. Pass +engine subclasses via ``inbound_cls`` / ``outbound_cls`` to customize one +mapping without forking (design rule 4). + +The output is still a valid AG-UI event sequence — "non-streaming" only +means it is produced in one shot after the run finishes, so there is no +token-level text and no mid-tool ``STATE_DELTA``. For live output use the +main :class:`~ag_ui_openai_agents.AGUITranslator`. +""" + +from __future__ import annotations + +from typing import Any + +from agents.items import RunItem +from ag_ui.core import BaseEvent, RunAgentInput + +from .engine.agui_to_sdk import AGUIToSDKTranslator +from .engine.sdk_to_agui import SDKToAGUITranslator +from .engine.types import TranslatedInput + + +class AGUINonStreamingTranslator: + """ + Pairs with ``Runner.run`` / ``Runner.run_sync``. + + :: + + translator = AGUINonStreamingTranslator() + bundle = translator.to_sdk(run_input) + result = await Runner.run(agent, input=bundle.messages, ...) + events = translator.to_agui(result) # complete AG-UI sequence + """ + + def __init__( + self, + *, + inbound_cls: type[AGUIToSDKTranslator] = AGUIToSDKTranslator, + outbound_cls: type[SDKToAGUITranslator] = SDKToAGUITranslator, + ) -> None: + self._inbound = inbound_cls() + self._outbound_cls = outbound_cls + + def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: + """AG-UI ``RunAgentInput`` → SDK-ready bundle (items, tools, state...). + + The bundle's ``tools`` holds :class:`agents.FunctionTool` proxies for + the client-declared tools — merge them with the agent's static tools + (``agent.clone(tools=[*agent.tools, *bundle.tools])``). + """ + bundle = self._inbound.translate(run_input) + if run_input.tools: + bundle = bundle.model_copy( + update={"tools": self._inbound.translate_tools(run_input.tools)} + ) + return bundle + + def to_agui(self, result: Any) -> list[BaseEvent]: + """Finished SDK run → complete AG-UI event sequence. + + Accepts a ``RunResult`` (its ``new_items`` are read) or a plain + ``list[RunItem]``. Every item emits full triplets — no windows stay + open, so there is nothing to finalize. + """ + items: list[RunItem] = ( + result if isinstance(result, list) else list(result.new_items) + ) + return self._outbound_cls().translate_items(items) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py new file mode 100644 index 0000000000..506de4e61a --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -0,0 +1,88 @@ +""" +Streaming facade — the package's main translator. + +:class:`AGUITranslator` pairs with ``Runner.run_streamed`` and exposes +exactly two public methods, one per direction: + + ``to_sdk(run_input)`` AG-UI ``RunAgentInput`` → SDK-ready bundle + ``to_agui(stream_events)`` SDK event stream → live AG-UI event stream + +All translation logic lives in the engine layer (:mod:`.engine`); this +class only orchestrates it. Window bookkeeping +and the final flush happen inside the ``to_agui`` iterator — callers never +see ``finalize``. + +The facade is stateless and reusable: each ``to_agui`` call creates the +fresh stateful engine that run needs. Pass engine subclasses via +``inbound_cls`` / ``outbound_cls`` to customize one mapping without forking +(design rule 4). + +Lifecycle events (``RUN_STARTED``/``RUN_FINISHED``/``RUN_ERROR``), +``STATE_SNAPSHOT`` and session persistence stay in the caller's run loop +(design rule 2) — see ``examples/server.py``. + +Non-streaming runs (``Runner.run`` / ``run_sync``): use +:class:`~ag_ui_openai_agents.AGUINonStreamingTranslator`. +""" + +from __future__ import annotations + +from typing import Any, AsyncIterable, AsyncIterator + +from ag_ui.core import BaseEvent, RunAgentInput + +from .engine.agui_to_sdk import AGUIToSDKTranslator +from .engine.sdk_to_agui import SDKToAGUITranslator +from .engine.types import TranslatedInput + + +class AGUITranslator: + """ + Main translator — pairs with ``Runner.run_streamed``. + + :: + + translator = AGUITranslator() + bundle = translator.to_sdk(run_input) + result = Runner.run_streamed(agent, input=bundle.messages, ...) + async for event in translator.to_agui(result.stream_events()): + ... # AG-UI BaseEvent, ready to encode + """ + + def __init__( + self, + *, + inbound_cls: type[AGUIToSDKTranslator] = AGUIToSDKTranslator, + outbound_cls: type[SDKToAGUITranslator] = SDKToAGUITranslator, + ) -> None: + self._inbound = inbound_cls() + self._outbound_cls = outbound_cls + + def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: + """AG-UI ``RunAgentInput`` → SDK-ready bundle (items, tools, state...). + + The bundle's ``tools`` holds :class:`agents.FunctionTool` proxies for + the client-declared tools — merge them with the agent's static tools + (``agent.clone(tools=[*agent.tools, *bundle.tools])``). + """ + bundle = self._inbound.translate(run_input) + if run_input.tools: + bundle = bundle.model_copy( + update={"tools": self._inbound.translate_tools(run_input.tools)} + ) + return bundle + + async def to_agui(self, stream_events: AsyncIterable[Any]) -> AsyncIterator[BaseEvent]: + """SDK event stream → live AG-UI event stream. + + Feed ``result.stream_events()`` from ``Runner.run_streamed``. A fresh + stateful engine handles this run's windows; when the stream ends the + engine flush runs automatically — any still-open text / tool-call / + reasoning window is closed before the iterator finishes. + """ + outbound = self._outbound_cls() + async for sdk_event in stream_events: + for event in outbound.translate(sdk_event): + yield event + for event in outbound.finalize(): + yield event diff --git a/integrations/openai-agents/python/tests/test_stream_types_drift.py b/integrations/openai-agents/python/tests/test_stream_types_drift.py index 999f55950a..ff5737b067 100644 --- a/integrations/openai-agents/python/tests/test_stream_types_drift.py +++ b/integrations/openai-agents/python/tests/test_stream_types_drift.py @@ -43,7 +43,7 @@ ResponseReasoningTextDoneEvent, ) -from ag_ui_openai_agents.translator import ( +from ag_ui_openai_agents.engine import ( HOSTED_TOOL_CALL_TYPES, RawResponseEventType, SDKItemType, diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py new file mode 100644 index 0000000000..6e9240853b --- /dev/null +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -0,0 +1,122 @@ +""" +Tests for the two-method facade translators. + +Covers the facade contract only — engine mappings have their own coverage: + +- ``to_sdk`` delegates to the inbound engine and populates ``tools``. +- ``AGUITranslator.to_agui`` streams engine output live and appends the + engine flush, with a fresh engine per call (reusable facade). +- ``AGUINonStreamingTranslator.to_agui`` accepts a ``RunResult``-shaped + object or a plain item list. +""" + +from __future__ import annotations + +import asyncio +from types import SimpleNamespace + +from ag_ui.core import CustomEvent, EventType, RunAgentInput, Tool, UserMessage +from agents import FunctionTool + +from ag_ui_openai_agents import AGUINonStreamingTranslator, AGUITranslator + + +def _run_input(with_tool: bool = False) -> RunAgentInput: + return RunAgentInput( + thread_id="t1", + run_id="r1", + messages=[UserMessage(id="m1", role="user", content="hi")], + tools=[ + Tool( + name="confirm", + description="Ask the user to confirm.", + parameters={"type": "object", "properties": {}}, + ) + ] + if with_tool + else [], + state={}, + context=[], + forwarded_props=None, + ) + + +def _event(name: str) -> CustomEvent: + return CustomEvent(type=EventType.CUSTOM, name=name, value=None) + + +class _StubOutbound: + """Records lifecycle; one AG-UI event per SDK event + a flush marker.""" + + instances = 0 + + def __init__(self) -> None: + type(self).instances += 1 + + def translate(self, sdk_event): + return [_event(f"translated:{sdk_event}")] + + def finalize(self): + return [_event("finalized")] + + def translate_items(self, items): + return [_event(f"item:{item}") for item in items] + + +async def _fake_stream(*names: str): + for name in names: + yield name + + +# ── to_sdk ─────────────────────────────────────────────────────────────── + + +def test_to_sdk_translates_messages_and_tools(): + translator = AGUITranslator() + bundle = translator.to_sdk(_run_input(with_tool=True)) + assert bundle.thread_id == "t1" + assert bundle.messages, "user message should be translated into input items" + assert len(bundle.tools) == 1 + assert isinstance(bundle.tools[0], FunctionTool) + assert bundle.tools[0].name == "confirm" + + +def test_to_sdk_without_tools_leaves_bundle_empty(): + bundle = AGUINonStreamingTranslator().to_sdk(_run_input()) + assert bundle.tools == [] + + +# ── AGUITranslator.to_agui (streaming) ─────────────────────────────────── + + +def test_streaming_to_agui_streams_then_finalizes(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + + async def collect(): + return [e async for e in translator.to_agui(_fake_stream("a", "b"))] + + events = asyncio.run(collect()) + assert [e.name for e in events] == ["translated:a", "translated:b", "finalized"] + + +def test_streaming_facade_is_reusable_fresh_engine_per_run(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + before = _StubOutbound.instances + + async def one_run(): + return [e async for e in translator.to_agui(_fake_stream("x"))] + + asyncio.run(one_run()) + asyncio.run(one_run()) + assert _StubOutbound.instances == before + 2 + + +# ── AGUINonStreamingTranslator.to_agui ─────────────────────────────────── + + +def test_non_streaming_to_agui_accepts_run_result_and_list(): + translator = AGUINonStreamingTranslator(outbound_cls=_StubOutbound) + + result = SimpleNamespace(new_items=["i1", "i2"]) + assert [e.name for e in translator.to_agui(result)] == ["item:i1", "item:i2"] + assert [e.name for e in translator.to_agui(["i3"])] == ["item:i3"] From 917adbd3bf26842a642713077f82c2b2b5a5d36c Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 6 Jul 2026 23:42:32 +0200 Subject: [PATCH 06/94] feat(translator): enhance docstrings for clarity and consistency across modules --- integrations/openai-agents/python/README.md | 4 +- .../src/ag_ui_openai_agents/__init__.py | 6 +- .../ag_ui_openai_agents/engine/__init__.py | 21 +- .../ag_ui_openai_agents/engine/agui_to_sdk.py | 420 ++++++++++----- .../src/ag_ui_openai_agents/engine/helpers.py | 42 +- .../ag_ui_openai_agents/engine/sdk_to_agui.py | 506 +++++++++++------- .../engine/stream_types.py | 43 +- .../src/ag_ui_openai_agents/engine/types.py | 113 ++-- .../non_streaming_translator.py | 58 +- .../src/ag_ui_openai_agents/translator.py | 66 ++- .../python/tests/test_translators.py | 6 +- 11 files changed, 753 insertions(+), 532 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index d89954357d..15956a2fa5 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -161,7 +161,7 @@ stay in your code — see the quick start above for the minimal shape. ## Advanced: the engine layer -The facades delegate to two independent, symmetric engine translators in +The public translators delegate to two independent, symmetric engine translators in `ag_ui_openai_agents.engine`: - `AGUIToSDKTranslator` — inbound; stateless, tiered per-type methods @@ -170,7 +170,7 @@ The facades delegate to two independent, symmetric engine translators in windows), per-type methods (`translate_text_delta`, `translate_item`, ...) Every per-type method is a public override point. To customize one mapping, -subclass the engine and inject it — the facade and every other mapping stay +subclass the engine and inject it — the public translator and every other mapping stay untouched: ```python diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py index 460343fc10..d6f8036515 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py @@ -1,10 +1,10 @@ """OpenAI Agents SDK × AG-UI Protocol integration. -Primary API — two-method facades, one per run mode:: +Primary API — translators, one per run mode: from ag_ui_openai_agents import AGUITranslator, AGUINonStreamingTranslator -Advanced (per-mapping overrides) — the engine layer:: +Advanced (per-mapping overrides) — the engine layer: from ag_ui_openai_agents.engine import AGUIToSDKTranslator, SDKToAGUITranslator """ @@ -23,7 +23,7 @@ __version__ = "0.1.0" __all__ = [ - # Facade translators (primary API — 2 methods each) + # Public translators (primary API — 2 methods each) "AGUITranslator", "AGUINonStreamingTranslator", # Engine translators (advanced / per-mapping overrides) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py index 0bc8ddda33..63ea9ff75f 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py @@ -1,20 +1,9 @@ -""" -Engine layer — the per-direction translation logic under the facades. - -Advanced API: subclass an engine to customize one mapping (design rule 4) -and inject it into a facade via ``inbound_cls`` / ``outbound_cls``. For -normal use import the facades from the package root instead -(:class:`~ag_ui_openai_agents.AGUITranslator`, -:class:`~ag_ui_openai_agents.AGUINonStreamingTranslator`). - -:: +"""Engine layer — the per-direction translation logic. - from ag_ui_openai_agents.engine import ( - AGUIToSDKTranslator, # inbound: AG-UI primitives → SDK shapes - SDKToAGUITranslator, # outbound: SDK formats → AG-UI primitives - TranslatedInput, # Pydantic bundle returned by translate()/to_sdk() - ClientToolPending, # sentinel raised by client-tool proxies - ) +Advanced use only: subclass an engine to customize a single mapping and +inject it into a public translator via inbound_cls / outbound_cls. For everything +else, import the translators from the package root instead (AGUITranslator, +AGUINonStreamingTranslator). """ from __future__ import annotations diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py index 5f870c2e81..300d9feb3c 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py @@ -1,38 +1,9 @@ """Inbound translator: AG-UI request primitives → OpenAI Agents SDK formats. -Layered API (each tier is callable on its own — they build on each other): - - Tier 1 — One-shot: - translate(run_input) → TranslatedInput (everything wired up) - - Tier 2 — Bulk collections: - translate_messages(messages) → list[input item] - translate_tools(tools) → list[FunctionTool] - translate_context(items) → str (formatted text block) - - Tier 3 — Single items (per AG-UI type): - translate_message(msg) → list[input item] (dispatcher) - translate_user_message - translate_system_message - translate_developer_message - translate_assistant_message - translate_tool_message - translate_reasoning_message - translate_activity_message - translate_tool(tool) → FunctionTool - translate_content(content) → list[input part] (dispatcher) - translate_content_part(part) → input part | None (dispatcher) - translate_text_content - translate_image_content - translate_audio_content - translate_video_content - translate_document_content - translate_binary_content - - Tier 4 — Internal helpers (underscore-prefixed). - -The translator is **stateless** — instantiate it once and reuse, or call -methods on a throwaway instance per request. Either works. +Layered so you can grab as much or as little as you need: translate() does +the whole request in one call, translate_() handles a collection, +and translate_() does a single item (override one to tweak just that +mapping). Stateless — make one and reuse it, or make one per request. """ from __future__ import annotations @@ -74,10 +45,15 @@ class ClientToolPending(Exception): """Raised by a client-tool proxy to signal "stop, the UI owns this call". - The outer run loop catches it, cancels the SDK run after the current turn, - and persists the resulting ``RunState`` keyed by ``thread_id`` so the next - AG-UI request (which carries an AG-UI ``ToolMessage`` with the client's - result) can resume from the same point. + The outer run loop catches it, cancels the SDK run after the current + turn, and persists the resulting RunState keyed by thread_id so the + next AG-UI request (which carries an AG-UI ToolMessage with the + client's result) can resume from the same point. + + Args: + tool_name: Name of the client-owned tool that was called. + tool_call_id: The SDK's call_id for this invocation. + arguments: Raw JSON arguments string the model produced. """ def __init__(self, tool_name: str, tool_call_id: str, arguments: str) -> None: @@ -94,24 +70,23 @@ def __init__(self, tool_name: str, tool_call_id: str, arguments: str) -> None: # --------------------------------------------------------------------------- class AGUIToSDKTranslator: - """Translate AG-UI inbound primitives into shapes the OpenAI Agents SDK expects. + """Translate AG-UI inbound primitives into OpenAI Agents SDK shapes. - Examples - -------- - One-shot:: + Example: + One-shot: - bundle = AGUIToSDKTranslator().translate(run_input) - result = Runner.run_streamed( - agent.clone(tools=agent.tools + bundle.function_tools), - input=bundle.input_items, - ) + bundle = AGUIToSDKTranslator().translate(run_input) + result = Runner.run_streamed( + agent.clone(tools=agent.tools + bundle.function_tools), + input=bundle.input_items, + ) - Per-item:: + Per-item: - translator = AGUIToSDKTranslator() - items = [translator.translate_user_message(msg) for msg in user_msgs] - proxy = translator.translate_tool(my_tool) - image_part = translator.translate_image_content(part) + translator = AGUIToSDKTranslator() + items = [translator.translate_user_message(msg) for msg in user_msgs] + proxy = translator.translate_tool(my_tool) + image_part = translator.translate_image_content(part) """ # ───────────────────────────────────────────────────────────────────── @@ -119,22 +94,28 @@ class AGUIToSDKTranslator: # ───────────────────────────────────────────────────────────────────── def translate(self, run_input: RunAgentInput) -> TranslatedInput: - """ - Translate an entire ``RunAgentInput`` into an SDK-ready bundle. + """Translate an entire RunAgentInput into an SDK-ready bundle. This is the high-level entry point. It does everything: converts - messages, wraps tools, and forwards state / context / forwarded_props / - thread_id / run_id / parent_run_id / resume so callers have one object - that mirrors :class:`ag_ui.core.RunAgentInput` field-for-field. - - Note on ``context`` and ``resume``: both pass through **unchanged**. - ``context`` items (from ``useCopilotReadable`` etc.) are not auto-folded - into the system prompt — call :meth:`translate_context` to format them - if your agent needs the model to see them. + messages, wraps tools, and forwards state / context / + forwarded_props / thread_id / run_id / parent_run_id / resume so + callers have one object that mirrors ag_ui.core.RunAgentInput + field-for-field. + + context and resume both pass through unchanged. context items + (from useCopilotReadable etc.) are not auto-folded into the + system prompt — call translate_context to format them if your + agent needs the model to see them. + + Args: + run_input: The incoming AG-UI RunAgentInput. + + Returns: + TranslatedInput with translated messages and passthrough + state/context/forwarded_props. """ - # ``resume`` was added in the TypeScript SDK first; the Python SDK - # may not expose it yet — read defensively so older SDK versions - # don't break us. + # Not every version of RunAgentInput has `resume`, so reach for it with + # getattr instead of assuming it's there. resume = getattr(run_input, "resume", None) return TranslatedInput( thread_id=run_input.thread_id, @@ -156,7 +137,14 @@ def translate_messages( self, messages: Iterable[Message], ) -> list[TResponseInputItem]: - """Translate every message and flatten into one Responses-API input list.""" + """Translate every message and flatten into one Responses-API input list. + + Args: + messages: AG-UI messages to translate. + + Returns: + Flattened list of Responses-API input items. + """ items = [] for message in messages: items.extend(self.translate_message(message)) @@ -166,7 +154,14 @@ def translate_tools( self, tools: Iterable[AGUITool], ) -> list[FunctionTool]: - """Wrap every AG-UI tool as a long-running SDK :class:`FunctionTool` proxy.""" + """Wrap every AG-UI tool as a long-running SDK FunctionTool proxy. + + Args: + tools: AG-UI client-declared tools. + + Returns: + SDK FunctionTool proxies, one per input tool. + """ return [self.translate_tool(tool) for tool in tools] def translate_context( @@ -175,26 +170,31 @@ def translate_context( ) -> str: """Render ambient context items as a plain-text block for the system prompt. - AG-UI ``context`` carries ``{description, value}`` pairs that frontends - (CopilotKit's ``useCopilotReadable``, etc.) send to give the model - ambient knowledge about the user's UI state — current page, selected - item, user identity, etc. + AG-UI context carries {description, value} pairs that frontends + (CopilotKit's useCopilotReadable, etc.) send to give the model + ambient knowledge about the user's UI state — current page, + selected item, user identity, etc. - We do **not** auto-inject this anywhere — callers decide whether to - prepend the rendered string to a system message, store it in agent - state, or ignore it. Returns an empty string when the input is empty. + This does not auto-inject anywhere — callers decide whether to + prepend the rendered string to a system message, store it in + agent state, or ignore it. - Output format:: + The output format is one "Description: value" line per item: Description A: value A Description B: value B - Example:: - + Example: prompt = "You are helpful." ctx = translator.translate_context(bundle.context) if ctx: prompt = f"{prompt}\\n\\nContext:\\n{ctx}" + + Args: + items: AG-UI context items. + + Returns: + Rendered text block, or an empty string when input is empty. """ lines = [ f"{item.description}: {item.value}" @@ -210,9 +210,15 @@ def translate_context( def translate_message(self, message: Message) -> list[dict[str, Any]]: """Dispatch one AG-UI message to the right per-type translator. - Returns a **list** because one message can produce multiple Responses-API - items (an AssistantMessage with N tool_calls splits into 1 message item - + N function_call items). + Returns a list because one message can produce multiple + Responses-API items (an AssistantMessage with N tool_calls + splits into 1 message item + N function_call items). + + Args: + message: An AG-UI message of any supported type. + + Returns: + Zero or more Responses-API input items. """ if isinstance(message, UserMessage): return [self.translate_user_message(message)] @@ -234,10 +240,16 @@ def translate_message(self, message: Message) -> list[dict[str, Any]]: return [] def translate_user_message(self, message: UserMessage) -> dict[str, Any]: - """User turn → ``{"type": "message", "role": "user", "content": [...]}``. + """Translate a user turn into a Responses-API message item. + + Supports multimodal: message.content may be a string or a list + of typed content parts (text / image / audio / ...). - Supports multimodal: ``message.content`` may be a string or a list of - typed content parts (text / image / audio / ...). + Args: + message: The AG-UI user message. + + Returns: + {"type": "message", "role": "user", "content": [...]} """ return { "type": "message", @@ -246,7 +258,14 @@ def translate_user_message(self, message: UserMessage) -> dict[str, Any]: } def translate_system_message(self, message: SystemMessage) -> dict[str, Any]: - """System prompt → ``{"type": "message", "role": "system", ...}``.""" + """Translate a system prompt into a Responses-API message item. + + Args: + message: The AG-UI system message. + + Returns: + {"type": "message", "role": "system", ...} + """ return { "type": "message", "role": "system", @@ -254,10 +273,16 @@ def translate_system_message(self, message: SystemMessage) -> dict[str, Any]: } def translate_developer_message(self, message: DeveloperMessage) -> dict[str, Any]: - """Developer prompt → ``{"type": "message", "role": "developer", ...}``. + """Translate a developer prompt into a Responses-API message item. - OpenAI's newer model lineage (GPT-4o+, o1) accepts ``role: developer`` - as a higher-priority alternative to ``system``. + OpenAI's newer model lineage (GPT-4o+, o1) accepts role: + developer as a higher-priority alternative to system. + + Args: + message: The AG-UI developer message. + + Returns: + {"type": "message", "role": "developer", ...} """ return { "type": "message", @@ -269,11 +294,18 @@ def translate_assistant_message( self, message: AssistantMessage, ) -> list[dict[str, Any]]: - """Assistant turn → optional text message + one ``function_call`` per tool call. + """Translate an assistant turn into message + function_call items. + + The Responses API splits assistant tool calls into separate + items (one message for any spoken text, plus one function_call + per invocation) — so one AG-UI message can produce N+1 items. + + Args: + message: The AG-UI assistant message. - The Responses API splits assistant tool calls into separate items - (one ``message`` for any spoken text, plus one ``function_call`` per - invocation) — so one AG-UI message can produce N+1 items. + Returns: + Optional text message item followed by one function_call + item per tool call. """ items: list[dict[str, Any]] = [] text = message.content or "" @@ -297,7 +329,14 @@ def translate_assistant_message( return items def translate_tool_message(self, message: ToolMessage) -> dict[str, Any]: - """Tool result → ``{"type": "function_call_output", ...}``.""" + """Translate a tool result into a function_call_output item. + + Args: + message: The AG-UI tool message. + + Returns: + {"type": "function_call_output", ...} + """ return { "type": "function_call_output", "call_id": message.tool_call_id, @@ -308,12 +347,18 @@ def translate_reasoning_message( self, message: ReasoningMessage, ) -> dict[str, Any] | None: - """Reasoning trace → ``{"type": "reasoning", ...}`` if usable, else ``None``. + """Translate a reasoning trace into a reasoning item, if replayable. + + OpenAI o-series models can re-ingest encrypted reasoning blobs + (the encrypted_value field) but treat plaintext reasoning as + opaque. A reasoning item is only emitted when an encrypted value + is present; otherwise the message is dropped with a debug log. - OpenAI o-series models can re-ingest *encrypted* reasoning blobs (the - ``encrypted_value`` field) but treat plaintext reasoning as opaque. - We only emit a reasoning item when an encrypted value is present; - otherwise we drop the message with a debug log. + Args: + message: The AG-UI reasoning message. + + Returns: + {"type": "reasoning", ...}, or None if not replayable. """ if not message.encrypted_value: logger.debug( @@ -332,11 +377,18 @@ def translate_activity_message( self, message: ActivityMessage, ) -> dict[str, Any] | None: - """Activity status → dropped by default (no Responses-API equivalent). + """Translate an activity status message — dropped by default. + + Activity messages describe UI side-effects (e.g. "user + navigated") that the model doesn't need to ingest. Subclasses + can override to fold them into the system prompt if a + particular use case needs it. + + Args: + message: The AG-UI activity message. - Activity messages describe UI side-effects (e.g. "user navigated") that - the model doesn't need to ingest. Subclasses can override to fold them - into the system prompt if a particular use case needs it. + Returns: + Always None; no Responses-API equivalent exists. """ logger.debug( "Dropping ActivityMessage id=%s activity_type=%s", @@ -350,11 +402,17 @@ def translate_activity_message( # ───────────────────────────────────────────────────────────────────── def translate_tool(self, tool: AGUITool) -> FunctionTool: - """ - Wrap one AG-UI :class:`Tool` as an SDK :class:`FunctionTool` proxy. + """Wrap one AG-UI Tool as an SDK FunctionTool proxy. + + The proxy's invocation handler raises ClientToolPending so the + outer run loop can pause the SDK and hand control back to the + client. - The proxy's invocation handler raises :class:`ClientToolPending` so the - outer run loop can pause the SDK and hand control back to the client. + Args: + tool: The AG-UI client-declared tool. + + Returns: + An SDK FunctionTool proxy for the tool. """ schema = self._ensure_object_schema(tool.parameters) @@ -382,11 +440,17 @@ async def on_invoke_tool( # ───────────────────────────────────────────────────────────────────── def translate_content(self, content: Any) -> list[dict[str, Any]]: - """Translate a message ``content`` field (str or list of parts) to input parts. + """Translate a message content field (str or list of parts) to input parts. + + A string is wrapped as a single input_text part. A list has + each part passed through translate_content_part. Anything else + is best-effort stringified. - * String → wrapped as a single ``input_text`` part. - * List → each part passed through :meth:`translate_content_part`. - * Anything else → best-effort stringification. + Args: + content: The message's content field. + + Returns: + List of Responses-API input parts. """ if isinstance(content, str): return [{"type": "input_text", "text": content}] @@ -401,7 +465,14 @@ def translate_content(self, content: Any) -> list[dict[str, Any]]: return [{"type": "input_text", "text": coerce_to_str(content)}] def translate_content_part(self, part: Any) -> dict[str, Any] | None: - """Dispatch one content part to its per-type translator.""" + """Dispatch one content part to its per-type translator. + + Args: + part: A single typed or dict-shaped content part. + + Returns: + The translated input part, or None if unsupported. + """ if isinstance(part, TextInputContent): return self.translate_text_content(part) if isinstance(part, ImageInputContent): @@ -414,21 +485,37 @@ def translate_content_part(self, part: Any) -> dict[str, Any] | None: return self.translate_document_content(part) if isinstance(part, BinaryInputContent): return self.translate_binary_content(part) - # Duck-typed fallback for dict-style parts (e.g. from JSON without strict parsing). + # Not a typed part — probably a raw dict from loosely-parsed JSON. + # Fall back to sniffing it by shape. return self._dispatch_dict_content_part(part) def translate_text_content(self, part: TextInputContent) -> dict[str, Any]: - """``TextInputContent`` → ``{"type": "input_text", "text": ...}``.""" + """Translate a TextInputContent part. + + Args: + part: The AG-UI text content part. + + Returns: + {"type": "input_text", "text": ...} + """ return {"type": "input_text", "text": part.text or ""} def translate_image_content( self, part: ImageInputContent, ) -> dict[str, Any] | None: - """``ImageInputContent`` → ``{"type": "input_image", "image_url": ...}``. + """Translate an ImageInputContent part. + + URL sources pass through unchanged. Data sources become base64 + data URLs so the Responses-API image_url field always receives + a single string. - URL sources pass through unchanged. Data sources become base64 data URLs - so the Responses-API ``image_url`` field always receives a single string. + Args: + part: The AG-UI image content part. + + Returns: + {"type": "input_image", "image_url": ...}, or None if the + source has no usable value. """ url = self._data_source_to_url(part.source) if url is None: @@ -439,11 +526,19 @@ def translate_audio_content( self, part: AudioInputContent, ) -> dict[str, Any] | None: - """``AudioInputContent`` → ``{"type": "input_audio", "input_audio": {...}}``. + """Translate an AudioInputContent part. + + The Responses API accepts base64 audio data with a short format + tag (wav, mp3). The format is extracted from the mime type. URL + sources are not supported by the API for audio — they get + dropped. + + Args: + part: The AG-UI audio content part. - The Responses API accepts base64 audio data with a short format tag - (``wav``, ``mp3``). We extract the format from the mime type. URL - sources are not supported by the API for audio — they get dropped. + Returns: + {"type": "input_audio", "input_audio": {...}}, or None if + unsupported. """ source_type = read_attr(part.source, "type") value = read_attr(part.source, "value") @@ -463,9 +558,16 @@ def translate_video_content( self, part: VideoInputContent, ) -> dict[str, Any] | None: - """``VideoInputContent`` → dropped (Responses API has no native video input). + """Translate a VideoInputContent part — dropped by default. - Override this in a subclass to swap in a placeholder or extract frames. + The Responses API has no native video input. Override this in + a subclass to swap in a placeholder or extract frames. + + Args: + part: The AG-UI video content part. + + Returns: + Always None. """ logger.debug("Dropping video part: Responses API does not accept video input") return None @@ -474,9 +576,15 @@ def translate_document_content( self, part: DocumentInputContent, ) -> dict[str, Any] | None: - """``DocumentInputContent`` → ``{"type": "input_file", ...}``. + """Translate a DocumentInputContent part. + + URL sources use file_url; data sources use file_data (base64). + + Args: + part: The AG-UI document content part. - URL sources use ``file_url``; data sources use ``file_data`` (base64). + Returns: + {"type": "input_file", ...}, or None if no usable value. """ source_type = read_attr(part.source, "type") value = read_attr(part.source, "value") @@ -496,11 +604,18 @@ def translate_binary_content( self, part: BinaryInputContent, ) -> dict[str, Any] | None: - """``BinaryInputContent`` → routed by mime type. + """Translate a BinaryInputContent part, routed by mime type. - Binary parts are a polymorphic catch-all: they may be images, audio, - documents, etc. We sniff the mime type and route to the corresponding - Responses-API input shape. Unknown types are dropped. + Binary parts are a polymorphic catch-all: they may be images, + audio, documents, etc. The mime type is sniffed and routed to + the corresponding Responses-API input shape. Unknown types are + dropped. + + Args: + part: The AG-UI binary content part. + + Returns: + The translated input part, or None if unsupported. """ mime = part.mime_type or "application/octet-stream" @@ -516,11 +631,18 @@ def translate_binary_content( # ───────────────────────────────────────────────────────────────────── def _ensure_object_schema(self, parameters: Any) -> dict[str, Any]: - """Normalise a possibly-empty tool parameter spec into a JSON Schema object. + """Normalize a possibly-empty tool parameter spec into a JSON Schema object. + + The Responses API requires every function tool's schema to be a + JSON Schema object. AG-UI tools sometimes ship parameter-less + specs as None or {}; those get coerced to an empty-but-valid + object. + + Args: + parameters: The tool's raw parameter spec. - The Responses API requires every function tool's schema to be a JSON - Schema object. AG-UI tools sometimes ship parameter-less specs as - ``None`` or ``{}``; we coerce those to an empty-but-valid object. + Returns: + A valid JSON Schema object. """ if not isinstance(parameters, dict) or "type" not in parameters: return {"type": "object", "properties": {}, "additionalProperties": True} @@ -528,10 +650,16 @@ def _ensure_object_schema(self, parameters: Any) -> dict[str, Any]: @staticmethod def _data_source_to_url(source: Any) -> str | None: - """Render a content source (data or url) as a single string for ``image_url``. + """Render a content source (data or url) as a single string for image_url. - * URL sources → pass through. - * Data sources → ``data:;base64,``. + URL sources pass through. Data sources become + data:;base64,. + + Args: + source: The content part's source object. + + Returns: + The resolved URL string, or None if there's no usable value. """ if source is None: return None @@ -548,11 +676,18 @@ def _data_source_to_url(source: Any) -> str | None: @staticmethod def _audio_format_from_mime(mime: str | None) -> str: - """Map a mime type to the short format string the Responses API wants.""" + """Map a mime type to the short format string the Responses API wants. + + Args: + mime: The audio mime type, or None. + + Returns: + A short format tag (e.g. "wav", "mp3"). + """ if not mime: return "wav" subtype = mime.split("/", 1)[-1].lower() - # Common normalisations. + # Same audio, lots of names for it — fold the common ones together. if subtype in ("mpeg", "mpeg3", "mp3"): return "mp3" if subtype in ("x-wav", "wav", "wave"): @@ -565,8 +700,15 @@ def _dispatch_dict_content_part( ) -> dict[str, Any] | None: """Best-effort dispatch for dict-shaped content parts (loose typing). - Sometimes content arrives as raw dicts rather than pydantic objects - (e.g. when callers hand-craft inputs). We sniff ``type`` and route. + Sometimes content arrives as raw dicts rather than pydantic + objects (e.g. when callers hand-craft inputs). The type field + is sniffed and routed. + + Args: + part: A dict-shaped (or duck-typed) content part. + + Returns: + The translated input part, or None if unsupported. """ part_type = read_attr(part, "type") if part_type == "text": @@ -577,10 +719,10 @@ def _dispatch_dict_content_part( if part_type == "image": url = self._data_source_to_url(read_attr(part, "source")) return {"type": "input_image", "image_url": url} if url else None - # Unknown / unsupported. + # Don't recognize it — skip it rather than guess. return None - # -- Binary-routing sub-helpers (internal, used only by translate_binary_content) + # -- Helpers for translate_binary_content, split out to keep it readable def _binary_as_image(self, part: BinaryInputContent) -> dict[str, Any] | None: if part.url: diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py index 8f92bd4510..4b54d7bbd2 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py @@ -16,26 +16,32 @@ # --------------------------------------------------------------------------- def new_message_id() -> str: - """Generate a fresh AG-UI message id (``msg_``).""" + """Generate a fresh AG-UI message id (msg_).""" return f"msg_{uuid.uuid4().hex}" def new_reasoning_id() -> str: - """Generate a fresh reasoning id (``rs_``, matching the wire prefix).""" + """Generate a fresh reasoning id (rs_, matching the wire prefix).""" return f"rs_{uuid.uuid4().hex}" def new_tool_call_id() -> str: - """Generate a fresh tool call id (``call_``).""" + """Generate a fresh tool call id (call_).""" return f"call_{uuid.uuid4().hex}" def new_tool_result_id(call_id: str) -> str: - """Derive the tool result message id (``-result``). + """Derive the tool result message id (-result). - Deterministic — the wire has no id on ``function_call_output``, so the - result id is derived from the ``call_id`` it answers. Hyphen never + Deterministic — the wire has no id on function_call_output, so the + result id is derived from the call_id it answers. Hyphen never appears in wire ids, marking the suffix as ours. + + Args: + call_id: The tool call id being answered. + + Returns: + The derived result message id. """ return f"{call_id}-result" @@ -45,11 +51,18 @@ def new_tool_result_id(call_id: str) -> str: # --------------------------------------------------------------------------- def read_attr(obj: Any, name: str) -> Any: - """Read ``name`` from a pydantic model **or** a dict — whichever we got. + """Read a field from a pydantic model or a dict — whichever we got. + + The SDK and AG-UI both hand us a mix of dicts and pydantic objects + depending on whether something has been through JSON serialization + yet. This shields callers from caring. + + Args: + obj: A pydantic model, a dict, or None. + name: The field/key name to read. - The SDK and AG-UI both hand us a mix of dicts and pydantic objects depending - on whether something has been through JSON serialization yet. This shields - callers from caring. + Returns: + The field value, or None if obj is None or the field is missing. """ if obj is None: return None @@ -59,7 +72,14 @@ def read_attr(obj: Any, name: str) -> Any: def coerce_to_str(value: Any) -> str: - """Best-effort stringification for tool outputs and content payloads.""" + """Best-effort stringification for tool outputs and content payloads. + + Args: + value: Any value — string, None, or JSON-serializable object. + + Returns: + A string representation, empty string for None. + """ if value is None: return "" if isinstance(value, str): diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py index 543537e071..8a341b00ca 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py @@ -1,63 +1,13 @@ -"""Outbound translator: OpenAI Agents SDK events → AG-UI ``BaseEvent``. - -Layered API (each tier is callable on its own — they build on each other), -mirroring :class:`~.agui_to_sdk.AGUIToSDKTranslator`: - - Tier 1 — One-shot: - translate(sdk_event) → list[BaseEvent] (dispatcher) - finalize() → list[BaseEvent] (flush open windows) - - Tier 2 — Bulk / per event family: - translate_items(items) → list[BaseEvent] (non-streaming runs) - translate_raw_response_event(e) → list[BaseEvent] - translate_run_item_event(e) → list[BaseEvent] - translate_agent_updated_event(e) → list[BaseEvent] - - Tier 3a — Raw Responses deltas (per raw ``type``): - translate_output_item_added / translate_output_item_done - translate_text_delta / translate_refusal_delta / translate_text_done - translate_function_call_arguments_delta - translate_reasoning_delta / translate_reasoning_part_done - - Tier 3b — Single run item (per SDK type): - translate_item(item) → list[BaseEvent] (dispatcher) - translate_message_output_item - translate_tool_call_item - translate_tool_call_output_item - translate_reasoning_item - translate_handoff_call_item - translate_handoff_output_item - translate_mcp_approval_request_item - translate_mcp_list_tools_item - translate_mcp_approval_response_item - - Tier 4 — Internal helpers (underscore-prefixed). - -The translator is **stateful per run** — it tracks open text / tool-call / -reasoning windows so AG-UI always sees strict ``START → CONTENT → END`` -triplets. Instantiate a fresh one per AG-UI run; never share across runs. - -Works with every SDK execution mode: - - Streaming (async):: - - translator = SDKToAGUITranslator() - result = Runner.run_streamed(agent, input=items) - async for sdk_event in result.stream_events(): - for event in translator.translate(sdk_event): - yield event - for event in translator.finalize(): - yield event - - Non-streaming (async or sync):: - - result = await Runner.run(agent, input=items) # or Runner.run_sync(...) - for event in SDKToAGUITranslator().translate_items(result.new_items): - yield event - -Run-envelope events (``RUN_STARTED`` / ``RUN_FINISHED`` / ``RUN_ERROR``, -``STATE_SNAPSHOT`` / ``STATE_DELTA``, ``MESSAGES_SNAPSHOT``) are the run -loop's job — the translator only translates. +"""Outbound translator: OpenAI Agents SDK events → AG-UI BaseEvent. + +Same layered shape as AGUIToSDKTranslator, just the other direction. +Stateful per run — it tracks open text / tool-call / reasoning windows so +AG-UI always sees strict START → CONTENT → END triplets. Make a fresh one +per run; don't share across runs. + +Run-envelope events (RUN_STARTED / RUN_FINISHED / RUN_ERROR, +STATE_SNAPSHOT / STATE_DELTA, MESSAGES_SNAPSHOT) are the run loop's job +— the translator only translates. """ from __future__ import annotations @@ -119,40 +69,43 @@ class SDKToAGUITranslator: - """Translate OpenAI Agents SDK outputs into AG-UI :class:`BaseEvent` objects. - - Wire ids are reused, never invented — with one caveat: some model backends - stamp every item with the SDK's ``FAKE_RESPONSES_ID`` placeholder instead - of a real id. Windows are therefore keyed by real item - id when available and by the raw event's ``output_index`` otherwise, and - placeholder ids are replaced with generated ones so AG-UI clients never see - two different messages sharing an id. - - The pattern is "lazy open, eager close": a ``*_START`` is emitted when the - first signal for a window arrives (``output_item.added`` or, defensively, - a bare delta), and ``*_END`` fires on the first close signal — - ``output_item.done``, the run-item commit, or :meth:`finalize`, whichever - comes first. Run items double as the full emission path for non-streaming - runs: when no window was ever opened for an item, its run-item translator - emits the complete triplet from the finished item. + """Translate OpenAI Agents SDK outputs into AG-UI BaseEvent objects. + + Wire ids are reused, never invented — with one caveat: some model + backends stamp every item with the SDK's FAKE_RESPONSES_ID + placeholder instead of a real id. Windows are therefore keyed by + real item id when available and by the raw event's output_index + otherwise, and placeholder ids are replaced with generated ones so + AG-UI clients never see two different messages sharing an id. + + The pattern is "lazy open, eager close": a *_START is emitted when + the first signal for a window arrives (output_item.added or, + defensively, a bare delta), and *_END fires on the first close + signal — output_item.done, the run-item commit, or finalize(), + whichever comes first. Run items double as the full emission path + for non-streaming runs: when no window was ever opened for an item, + its run-item translator emits the complete triplet from the + finished item. """ def __init__(self) -> None: - # Open windows, keyed by real item id or ``__idx_``. - # Values are the ids already emitted in the *_START events. - self._open_texts: dict[str, str] = {} # key → message_id - self._open_tool_calls: dict[str, str] = {} # key → tool_call_id - self._open_reasonings: dict[str, str] = {} # key → phase message_id - self._open_reasoning_parts: dict[str, str] = {} # key → part message_id - # Close bookkeeping — lets run items tell "close me" (streamed) apart - # from "emit me whole" (non-streamed) even with placeholder ids. + # What's currently open. Key is the real item id when we have one, + # otherwise __idx_. Value is the id we already sent in + # the matching *_START event, so we can pair the END to it. + self._open_texts: dict[str, str] = {} # key -> message_id + self._open_tool_calls: dict[str, str] = {} # key -> tool_call_id + self._open_reasonings: dict[str, str] = {} # key -> phase message_id + self._open_reasoning_parts: dict[str, str] = {} # key -> part message_id + # When a run item shows up, we need to know if we already streamed and + # closed it (nothing to do) or if this is a non-streaming run and we + # have to emit it whole. These track what's been closed so we can tell. self._closed_text_ids: list[str] = [] self._closed_reasoning_ids: list[str] = [] self._seen_call_ids: set[str] = set() self._emitted_encrypted_keys: set[str] = set() self._reasoning_part_seq: dict[str, int] = {} - # key → phase id, never popped — encrypted values can arrive after the - # phase was already force-closed and still need the right entity_id. + # Never cleared: an encrypted value can land after we've already closed + # the reasoning phase, and we still need the original id to attach it to. self._reasoning_phase_ids: dict[str, str] = {} self._current_step: str | None = None @@ -161,11 +114,17 @@ def __init__(self) -> None: # ───────────────────────────────────────────────────────────────────── def translate(self, sdk_event: Any) -> list[BaseEvent]: - """Dispatch one SDK :class:`StreamEvent` to the right family translator. + """Dispatch one SDK StreamEvent to the right family translator. + + Returns a list because one SDK event can produce several AG-UI + events (e.g. an output_item.added that force-opens a window). + Unknown event types translate to [] with a debug log. - Returns a **list** because one SDK event can produce several AG-UI - events (e.g. an ``output_item.added`` that force-opens a window). - Unknown event types translate to ``[]`` with a debug log. + Args: + sdk_event: One event from result.stream_events(). + + Returns: + Zero or more translated AG-UI events. """ event_type = read_attr(sdk_event, "type") if event_type == SDKStreamEventType.RAW_RESPONSE: @@ -178,10 +137,13 @@ def translate(self, sdk_event: Any) -> list[BaseEvent]: return [] def finalize(self) -> list[BaseEvent]: - """Emit pending ``*_END`` markers when the SDK stream terminates. + """Emit pending *_END markers when the SDK stream terminates. + + Always call this after the stream ends — even on error — so the + AG-UI client never sees a window that opened but never closed. - Always call this after the stream ends — even on error — so the AG-UI - client never sees a window that opened but never closed. + Returns: + Closing events for any windows still open. """ events: list[BaseEvent] = [] for key in list(self._open_texts): @@ -207,9 +169,15 @@ def finalize(self) -> list[BaseEvent]: def translate_items(self, items: list[RunItem]) -> list[BaseEvent]: """Translate a finished run's items (non-streaming mode). - Feed ``result.new_items`` from ``Runner.run`` / ``Runner.run_sync``. - Each item emits its complete AG-UI sequence (full triplets — no - windows stay open), so no :meth:`finalize` call is needed after. + Feed result.new_items from Runner.run / Runner.run_sync. Each + item emits its complete AG-UI sequence (full triplets — no + windows stay open), so no finalize() call is needed after. + + Args: + items: The run's finished RunItem list. + + Returns: + The complete list of AG-UI events for the run. """ events: list[BaseEvent] = [] for item in items: @@ -217,7 +185,14 @@ def translate_items(self, items: list[RunItem]) -> list[BaseEvent]: return events def translate_raw_response_event(self, event: Any) -> list[BaseEvent]: - """Handle a ``RawResponsesStreamEvent`` (token-level Responses deltas).""" + """Handle a RawResponsesStreamEvent (token-level Responses deltas). + + Args: + event: The SDK's RawResponsesStreamEvent. + + Returns: + Zero or more translated AG-UI events. + """ data = read_attr(event, "data") if data is None: return [] @@ -244,22 +219,36 @@ def translate_raw_response_event(self, event: Any) -> list[BaseEvent]: RawResponseEventType.REASONING_TEXT_DONE, ): return self.translate_reasoning_part_done(data) - # response.created / .completed / content_part bookkeeping / audio — no - # AG-UI equivalent at this layer. + # Everything else (response.created/.completed, content_part chatter, + # audio) has nothing to show on the AG-UI side, so we drop it. return [] def translate_run_item_event(self, event: Any) -> list[BaseEvent]: - """Handle a ``RunItemStreamEvent`` (semantic commit signals).""" + """Handle a RunItemStreamEvent (semantic commit signals). + + Args: + event: The SDK's RunItemStreamEvent. + + Returns: + Zero or more translated AG-UI events. + """ item = read_attr(event, "item") if item is None: return [] return self.translate_item(item) def translate_agent_updated_event(self, event: Any) -> list[BaseEvent]: - """``AgentUpdatedStreamEvent`` → ``STEP_FINISHED`` (prev) + ``STEP_STARTED``. + """Translate an AgentUpdatedStreamEvent into STEP_FINISHED + STEP_STARTED. + + Each agent (including the first, and each handoff target) is + surfaced as an AG-UI step named after the agent. - Each agent (including the first, and each handoff target) is surfaced - as an AG-UI step named after the agent. + Args: + event: The SDK's AgentUpdatedStreamEvent. + + Returns: + STEP_FINISHED for the previous step (if any) followed by + STEP_STARTED for the new one. """ new_agent = read_attr(event, "new_agent") step_name = read_attr(new_agent, "name") or "agent" @@ -278,16 +267,22 @@ def translate_agent_updated_event(self, event: Any) -> list[BaseEvent]: return events # ───────────────────────────────────────────────────────────────────── - # TIER 3a — Raw Responses deltas (per raw ``type``) + # TIER 3a — Raw Responses deltas (per raw type) # ───────────────────────────────────────────────────────────────────── def translate_output_item_added(self, data: Any) -> list[BaseEvent]: - """``response.output_item.added`` → open the matching AG-UI window. + """Translate response.output_item.added by opening the matching AG-UI window. + + message opens TEXT_MESSAGE_START, function_call opens + TOOL_CALL_START, reasoning opens REASONING_START, and hosted + tool calls open TOOL_CALL_START with the tool name set to the + item type. - * ``message`` → ``TEXT_MESSAGE_START`` - * ``function_call`` → ``TOOL_CALL_START`` - * ``reasoning`` → ``REASONING_START`` - * hosted tool calls → ``TOOL_CALL_START`` (tool name = item type) + Args: + data: The raw output_item.added payload. + + Returns: + The opening event(s) for the window, if any. """ item = read_attr(data, "item") item_type = read_attr(item, "type") @@ -308,11 +303,17 @@ def translate_output_item_added(self, data: Any) -> list[BaseEvent]: return [] def translate_output_item_done(self, data: Any) -> list[BaseEvent]: - """``response.output_item.done`` → close the matching AG-UI window. + """Translate response.output_item.done by closing the matching AG-UI window. + + For reasoning items this also emits REASONING_ENCRYPTED_VALUE + (subtype "message") when the finished item carries + encrypted_content. - For reasoning items this also emits ``REASONING_ENCRYPTED_VALUE`` - (subtype ``"message"``) when the finished item carries - ``encrypted_content``. + Args: + data: The raw output_item.done payload. + + Returns: + The closing event(s) for the window, if any. """ item = read_attr(data, "item") item_type = read_attr(item, "type") @@ -329,38 +330,65 @@ def translate_output_item_done(self, data: Any) -> list[BaseEvent]: return [] def translate_text_delta(self, data: Any) -> list[BaseEvent]: - """``response.output_text.delta`` → ``TEXT_MESSAGE_CONTENT`` (lazy start).""" + """Translate response.output_text.delta into TEXT_MESSAGE_CONTENT (lazy start). + + Args: + data: The raw output_text.delta payload. + + Returns: + Content event(s), opening the window first if needed. + """ key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) return self._emit_text_content(key, read_attr(data, "delta") or "") def translate_text_done(self, data: Any) -> list[BaseEvent]: - """``response.output_text.done`` → early ``TEXT_MESSAGE_END``. + """Translate response.output_text.done into an early TEXT_MESSAGE_END. + + Some model backends skip output_item.done; this closes the + window on the text-level done signal instead. Idempotent — + closing an already-closed window is a no-op. - Some model backends skip ``output_item.done``; this closes the - window on the text-level done signal instead. Idempotent — closing an - already-closed window is a no-op. + Args: + data: The raw output_text.done payload. + + Returns: + The closing event, or [] if already closed. """ key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) return self._close_text(key) def translate_refusal_delta(self, data: Any) -> list[BaseEvent]: - """``response.refusal.delta`` → ``TEXT_MESSAGE_CONTENT``. + """Translate response.refusal.delta into TEXT_MESSAGE_CONTENT. + + Refusal text is user-visible assistant output, so it streams + into the same text window as regular content. - Refusal text is user-visible assistant output, so it streams into the - same text window as regular content. + Args: + data: The raw refusal.delta payload. + + Returns: + Content event(s), opening the window first if needed. """ key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) return self._emit_text_content(key, read_attr(data, "delta") or "") def translate_function_call_arguments_delta(self, data: Any) -> list[BaseEvent]: - """``response.function_call_arguments.delta`` → ``TOOL_CALL_ARGS``.""" + """Translate response.function_call_arguments.delta into TOOL_CALL_ARGS. + + Args: + data: The raw function_call_arguments.delta payload. + + Returns: + Args event(s), opening the window first if needed. + """ delta = read_attr(data, "delta") or "" if not delta: return [] key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) events: list[BaseEvent] = [] if key not in self._open_tool_calls: - # Defensive lazy open — provider skipped output_item.added. + # Some providers jump straight to args without an output_item.added, + # so open the call here if we haven't already. events.extend(self._open_tool_call(key, new_tool_call_id(), "")) events.append( ToolCallArgsEvent( @@ -372,14 +400,20 @@ def translate_function_call_arguments_delta(self, data: Any) -> list[BaseEvent]: return events def translate_reasoning_delta(self, data: Any) -> list[BaseEvent]: - """Reasoning deltas → ``REASONING_MESSAGE_CONTENT`` (lazy part start). + """Translate reasoning deltas into REASONING_MESSAGE_CONTENT (lazy part start). - Covers both sources — ``response.reasoning_summary_text.delta`` + Covers both sources — response.reasoning_summary_text.delta (hosted models: model-written summaries) and - ``response.reasoning_text.delta`` (open-weight models: full chain of - thought). Whichever arrives streams; each part becomes its own - ``REASONING_MESSAGE_*`` window inside one ``REASONING_START/END`` + response.reasoning_text.delta (open-weight models: full chain + of thought). Whichever arrives streams; each part becomes its + own REASONING_MESSAGE_* window inside one REASONING_START/END phase, matching the other AG-UI integrations. + + Args: + data: The raw reasoning delta payload. + + Returns: + Content event(s), opening the phase/part first if needed. """ delta = read_attr(data, "delta") or "" if not delta: @@ -400,7 +434,14 @@ def translate_reasoning_delta(self, data: Any) -> list[BaseEvent]: return events def translate_reasoning_part_done(self, data: Any) -> list[BaseEvent]: - """Summary-part / reasoning-text done → ``REASONING_MESSAGE_END``.""" + """Translate a summary-part / reasoning-text done signal into REASONING_MESSAGE_END. + + Args: + data: The raw part-done payload. + + Returns: + The closing event, or [] if already closed. + """ key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) return self._close_reasoning_part(key) @@ -409,11 +450,17 @@ def translate_reasoning_part_done(self, data: Any) -> list[BaseEvent]: # ───────────────────────────────────────────────────────────────────── def translate_item(self, item: RunItem) -> list[BaseEvent]: - """Dispatch one SDK :class:`RunItem` to the right per-type translator. + """Dispatch one SDK RunItem to the right per-type translator. + + During streaming these act as safety-net closers (raw events + already streamed the content); for non-streaming runs they emit + the item's full AG-UI sequence. - During streaming these act as safety-net closers (raw events already - streamed the content); for non-streaming runs they emit the item's - full AG-UI sequence. + Args: + item: A finished SDK RunItem. + + Returns: + Zero or more translated AG-UI events. """ if isinstance(item, MessageOutputItem): return self.translate_message_output_item(item) @@ -437,12 +484,18 @@ def translate_item(self, item: RunItem) -> list[BaseEvent]: return [] def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEvent]: - """Assistant message commit → close its window, or emit the full triplet. + """Handle an assistant message commit by closing its window or emitting the full triplet. Streamed: the raw deltas already carried the text — just close. - Non-streamed: emit ``TEXT_MESSAGE_START/CONTENT/END`` from the finished - item, using the SDK's own :class:`ItemHelpers` extractors (text first, - refusal as fallback — both are user-visible). + Non-streamed: emit TEXT_MESSAGE_START/CONTENT/END from the + finished item, using the SDK's own ItemHelpers extractors (text + first, refusal as fallback — both are user-visible). + + Args: + item: The finished MessageOutputItem. + + Returns: + Closing event, or the full START/CONTENT/END triplet. """ raw = item.raw_item raw_id = read_attr(raw, "id") @@ -465,13 +518,18 @@ def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEve events.extend(self._close_text(message_id)) return events def translate_tool_call_item(self, item: ToolCallItem) -> list[BaseEvent]: - """Tool call commit → close its window, or emit ``START/[ARGS]/END``. + """Handle a tool call commit by closing its window or emitting START/[ARGS]/END. + + Reconciled by call_id (present and real regardless of backend), + using the SDK's built-in ToolCallItem.call_id / .tool_name + properties where available — getattr fallbacks because + HandoffCallItem routes through here too and lacks them. + + Args: + item: The finished ToolCallItem (or a HandoffCallItem). - Reconciled by ``call_id`` (present and real regardless of backend), - using the SDK's built-in :attr:`ToolCallItem.call_id` / - :attr:`ToolCallItem.tool_name` properties where available — - ``getattr`` fallbacks because :class:`HandoffCallItem` routes through - here too and lacks them. + Returns: + Closing event, or the full START/[ARGS]/END sequence. """ raw = item.raw_item call_id = ( @@ -504,7 +562,14 @@ def translate_tool_call_item(self, item: ToolCallItem) -> list[BaseEvent]: return events def translate_tool_call_output_item(self, item: ToolCallOutputItem) -> list[BaseEvent]: - """Tool result → ``TOOL_CALL_RESULT``.""" + """Translate a tool result into TOOL_CALL_RESULT. + + Args: + item: The finished ToolCallOutputItem. + + Returns: + A single TOOL_CALL_RESULT event, or [] if call_id is missing. + """ call_id = item.call_id if not call_id: logger.debug("Tool output without call_id; skipping TOOL_CALL_RESULT") @@ -519,11 +584,17 @@ def translate_tool_call_output_item(self, item: ToolCallOutputItem) -> list[Base ] def translate_reasoning_item(self, item: ReasoningItem) -> list[BaseEvent]: - """Reasoning commit → close its phase, or emit the full sequence. + """Handle a reasoning commit by closing its phase or emitting the full sequence. + + Non-streamed emission walks the finished ResponseReasoningItem: + one REASONING_MESSAGE_* window per summary/content entry, then + REASONING_ENCRYPTED_VALUE (when present), then REASONING_END. - Non-streamed emission walks the finished ``ResponseReasoningItem``: - one ``REASONING_MESSAGE_*`` window per summary/content entry, then - ``REASONING_ENCRYPTED_VALUE`` (when present), then ``REASONING_END``. + Args: + item: The finished ReasoningItem. + + Returns: + Closing event(s), or the full reasoning sequence. """ raw = item.raw_item raw_id = read_attr(raw, "id") @@ -555,16 +626,29 @@ def translate_reasoning_item(self, item: ReasoningItem) -> list[BaseEvent]: return events def translate_handoff_call_item(self, item: HandoffCallItem) -> list[BaseEvent]: - """Handoff request → surfaced as a tool call (it *is* a function call). + """Surface a handoff request as a tool call (it is a function call). + + The same underlying item also arrives through the raw-event + path, so the call-id reconciliation in translate_tool_call_item + prevents a duplicate TOOL_CALL_START. - The same underlying item also arrives through the raw-event path, so - the call-id reconciliation in :meth:`translate_tool_call_item` - prevents a duplicate ``TOOL_CALL_START``. + Args: + item: The finished HandoffCallItem. + + Returns: + Closing event, or the full START/[ARGS]/END sequence. """ return self.translate_tool_call_item(item) # type: ignore[arg-type] def translate_handoff_output_item(self, item: HandoffOutputItem) -> list[BaseEvent]: - """Handoff completion → ``TOOL_CALL_RESULT``.""" + """Translate a handoff completion into TOOL_CALL_RESULT. + + Args: + item: The finished HandoffOutputItem. + + Returns: + A single TOOL_CALL_RESULT event, or [] if call_id is missing. + """ raw = item.raw_item call_id = read_attr(raw, "call_id") if not call_id: @@ -585,10 +669,17 @@ def translate_mcp_approval_request_item( self, item: MCPApprovalRequestItem, ) -> list[BaseEvent]: - """MCP approval request → ``CUSTOM`` event (``name="mcp_approval_request"``). + """Translate an MCP approval request into a CUSTOM event (name="mcp_approval_request"). + + AG-UI has no native approval shape yet; forwarding the raw + request lets a frontend implement approval UI without protocol + changes. + + Args: + item: The finished MCPApprovalRequestItem. - AG-UI has no native approval shape yet; forwarding the raw request - lets a frontend implement approval UI without protocol changes. + Returns: + A single CUSTOM event carrying the raw request. """ raw = item.raw_item value = raw.model_dump() if hasattr(raw, "model_dump") else raw @@ -601,7 +692,14 @@ def translate_mcp_approval_request_item( ] def translate_mcp_list_tools_item(self, item: MCPListToolsItem) -> list[BaseEvent]: - """MCP tool listing → dropped (server-side bookkeeping). Overridable.""" + """Drop an MCP tool listing item (server-side bookkeeping). Overridable. + + Args: + item: The finished MCPListToolsItem. + + Returns: + Always []. + """ logger.debug("Dropping MCPListToolsItem id=%s", read_attr(item.raw_item, "id")) return [] @@ -609,7 +707,14 @@ def translate_mcp_approval_response_item( self, item: MCPApprovalResponseItem, ) -> list[BaseEvent]: - """MCP approval response → dropped (echo of client input). Overridable.""" + """Drop an MCP approval response item (echo of client input). Overridable. + + Args: + item: The finished MCPApprovalResponseItem. + + Returns: + Always []. + """ logger.debug("Dropping MCPApprovalResponseItem") return [] @@ -619,28 +724,50 @@ def translate_mcp_approval_response_item( @staticmethod def _is_real_id(item_id: Any) -> bool: - """True when the id is usable on the wire (not empty, not a placeholder). + """Check whether an id is usable on the wire (not empty, not a placeholder). Some model backends stamp every item with the SDK's - ``FAKE_RESPONSES_ID`` sentinel — sharing it across AG-UI events would - collide every message of the run. Checked against the SDK's own - constant, so it's a no-op (always real) on native OpenAI and correct - for any other backend without a provider-specific branch. + FAKE_RESPONSES_ID sentinel — sharing it across AG-UI events + would collide every message of the run. Checked against the + SDK's own constant, so it's a no-op (always real) on native + OpenAI and correct for any other backend without a + provider-specific branch. + + Args: + item_id: The item's wire id, or None. + + Returns: + True if the id is real and usable. """ return bool(item_id) and item_id != FAKE_RESPONSES_ID @classmethod def _resolve_id(cls, item_id: Any, generate: Any) -> str: - """Return the id if real, else a freshly generated one.""" + """Return the id if real, else a freshly generated one. + + Args: + item_id: The item's wire id, or None. + generate: A zero-arg callable producing a fresh id. + + Returns: + The real id, or a freshly generated one. + """ return item_id if cls._is_real_id(item_id) else generate() @classmethod def _window_key(cls, item_id: Any, output_index: Any) -> str: - """Stable window key for correlating raw events of one output item. + """Compute a stable window key for correlating raw events of one output item. + + Real item ids win; placeholder ids fall back to the stream + position (output_index), which the Responses API guarantees is + unique per item within a response. + + Args: + item_id: The item's wire id, or None. + output_index: The item's position in the response. - Real item ids win; placeholder ids fall back to the stream position - (``output_index``), which the Responses API guarantees is unique per - item within a response. + Returns: + A stable key for this item's window. """ if cls._is_real_id(item_id): return item_id @@ -654,14 +781,20 @@ def _reconcile( ) -> tuple[str, str | None]: """Match a run-item commit against streamed window state. - Returns ``("close", key)`` when a streamed window is still open, - ``("skip", None)`` when the item was already fully streamed and - closed, or ``("new", None)`` when nothing was streamed (non-streaming - run) and the item must be emitted whole. + With a real id the match is exact. With a placeholder id run + items arrive in stream order, so the oldest open window (or one + queued closed id) is consumed instead. - With a real id the match is exact. With a placeholder id run items - arrive in stream order, so the oldest open window (or one queued - closed id) is consumed instead. + Args: + raw_id: The run item's raw wire id, or None. + open_windows: Currently open windows for this category. + closed_ids: Ids already closed via streaming, oldest first. + + Returns: + ("close", key) when a streamed window is still open, + ("skip", None) when the item was already fully streamed and + closed, or ("new", None) when nothing was streamed + (non-streaming run) and the item must be emitted whole. """ if self._is_real_id(raw_id): if raw_id in open_windows: @@ -683,9 +816,9 @@ def _reconcile( def _open_text(self, key: str, message_id: str) -> list[BaseEvent]: if key in self._open_texts: return [] - # The model has moved on to producing output — any reasoning phase is - # over, even if the provider's reasoning done-event is still in flight - # (some backends deliver it late). + # Once real output starts, the model is done thinking. Close any open + # reasoning now instead of waiting for the done-event — some backends + # send it late, and we don't want reasoning bleeding into the answer. events = self._close_all_reasonings() self._open_texts[key] = message_id events.append( @@ -767,9 +900,9 @@ def _open_reasoning_part(self, key: str) -> list[BaseEvent]: return [] seq = self._reasoning_part_seq.get(key, 0) self._reasoning_part_seq[key] = seq + 1 - # First part reuses the phase id (round-trip friendly); later parts - # get a stable derived suffix. Hyphen never appears in wire ids, so - # the suffix is unambiguously ours. + # The first part just reuses the phase id (keeps round-trips clean); + # extra parts get a -1, -2, ... suffix. Wire ids never contain a + # hyphen, so anything with one is clearly something we made up. phase_id = self._open_reasonings.get(key, key) message_id = phase_id if seq == 0 else f"{phase_id}-{seq}" self._open_reasoning_parts[key] = message_id @@ -813,7 +946,16 @@ def _close_all_reasonings(self) -> list[BaseEvent]: return events def _emit_encrypted_value(self, key: str, item: Any) -> list[BaseEvent]: - """Emit ``REASONING_ENCRYPTED_VALUE`` once per reasoning item, if present.""" + """Emit REASONING_ENCRYPTED_VALUE once per reasoning item, if present. + + Args: + key: The reasoning item's window key. + item: The raw reasoning item. + + Returns: + A single REASONING_ENCRYPTED_VALUE event, or [] if absent + or already emitted. + """ encrypted = read_attr(item, "encrypted_content") if not encrypted or key in self._emitted_encrypted_keys: return [] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py index 3e64ec5e33..0a8f6116db 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py @@ -1,21 +1,9 @@ -""" -Wire-level discriminator values for OpenAI Agents SDK streaming. - -Single home for every ``type`` string the translators dispatch on, grouped by -the layer that produces it. Members are ``(str, Enum)`` (``StrEnum`` needs -Python 3.11; this package supports 3.9+), so they compare equal to the raw -wire strings — dispatch code can test ``kind == RawResponseEventType.TEXT_DELTA`` -against untyped event payloads with no conversion. - -The SDK is the source of truth for these values: +"""Wire-level discriminator values for OpenAI Agents SDK streaming. -- ``SDKStreamEventType`` — ``agents.stream_events.StreamEvent`` union -- ``RawResponseEventType`` — ``openai.types.responses`` event ``type`` literals -- ``SDKItemType`` — ``openai.types.responses`` output-item ``type`` - literals (also mirrored by ``agents.items`` run-item wrappers) - -When a new SDK version adds values not listed here, translators degrade -gracefully (debug log + skip) — see design rule 5 in ``CLAUDE.md``. +Single home for every "type" string the translators dispatch on. Members +are (str, Enum) (StrEnum needs Python 3.11; this package supports 3.9+), +so they compare equal to the raw wire strings. The SDK's own Literal[...] +annotations are the source of truth — see tests/test_stream_types_drift.py. """ from __future__ import annotations @@ -24,9 +12,7 @@ class SDKStreamEventType(str, Enum): - """ - Top-level ``StreamEvent.type`` values yielded by ``Runner.run_streamed``. - """ + """Top-level StreamEvent.type values yielded by Runner.run_streamed.""" RAW_RESPONSE = "raw_response_event" RUN_ITEM = "run_item_stream_event" @@ -34,11 +20,10 @@ class SDKStreamEventType(str, Enum): class RawResponseEventType(str, Enum): - """ - Raw Responses-API delta ``type`` values the outbound translator consumes. + """Raw Responses-API delta type values the outbound translator consumes. - Non-semantic bookkeeping kinds (``response.created`` / ``.completed``, - ``content_part.*``, audio) are deliberately absent — they translate to + Non-semantic bookkeeping kinds (response.created / .completed, + content_part.*, audio) are deliberately absent — they translate to nothing. """ @@ -55,18 +40,16 @@ class RawResponseEventType(str, Enum): class SDKItemType(str, Enum): - """ - Output-item ``type`` values carried by ``output_item.added`` / ``.done``. - """ + """Output-item type values carried by output_item.added / .done.""" MESSAGE = "message" FUNCTION_CALL = "function_call" REASONING = "reasoning" -# Hosted (server-side) tool calls surfaced as AG-UI tool calls. The API never -# streams their arguments, so they get START/END only at the raw level; the -# richer run-item layer fills in details when available. +# Tools that run on OpenAI's side. We still show them as AG-UI tool calls, but +# the API doesn't stream their arguments, so at the raw level all we can emit is +# START/END. The run-item layer fills in the rest when it has it. HOSTED_TOOL_CALL_TYPES: frozenset[str] = frozenset( { "web_search_call", diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index 6e7721a521..e25edbcb64 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -1,9 +1,8 @@ -""" -Public data containers for the translator package. +"""Public data containers for the translator package. -Holds typed result objects produced by the translators. Translator *behavior* -lives in :mod:`agui_to_sdk` and :mod:`sdk_to_agui`; this module only describes -the shapes those translators hand back. +Holds typed result objects produced by the translators. Translator +behavior lives in agui_to_sdk.py and sdk_to_agui.py; this module only +describes the shapes those translators hand back. """ from __future__ import annotations @@ -16,108 +15,62 @@ class TranslatedInput(BaseModel): - """ - SDK-ready bundle produced by translating an AG-UI ``RunAgentInput``. - - The field shape **mirrors** :class:`ag_ui.core.RunAgentInput` field-for- - field — same required/optional pattern — so callers familiar with the - wire format can map between the two without thinking. Two fields are - renamed because their *content* was translated, not just passed through: - - * ``messages`` → ``input_items`` — now Responses-API input items. - * ``tools`` → ``function_tools`` — now SDK :class:`FunctionTool` proxies. + """SDK-ready bundle produced by translating an AG-UI RunAgentInput. - Everything else is passed through unchanged so downstream code decides - what to do with raw frontend payloads (e.g. ``context``, ``forwarded_props``). - - Required fields (must always be provided): - thread_id, run_id, input_items, function_tools, - state, context, forwarded_props. - - Optional fields (default to ``None``): - parent_run_id, resume. + The fields line up one-for-one with ag_ui.core.RunAgentInput — same + names, same required/optional split — so if you know the wire format + you already know this. The only real work happens on `messages` and + `tools` (translated into SDK shapes); everything else is handed + straight through for you to use however your app needs. """ # ── Identity ───────────────────────────────────────────────────────── thread_id: str - """ - Session key for this conversation. Same value across multi-turn runs and - used by the session manager for HITL resume. - """ + """Conversation key. Stays the same across turns of one thread.""" run_id: str - """ - Unique id for this specific run. Tagged onto ``RUN_STARTED`` / - ``RUN_FINISHED`` events emitted back to the client. - """ + """Id for this single run. Goes on the RUN_STARTED / RUN_FINISHED events.""" parent_run_id: str | None = None - """ - Optional parent run id — set when this run was triggered by another run - (e.g. a nested or branched agent invocation). ``None`` for top-level runs. - """ + """Set when this run was kicked off by another run (nested/branched); + None for a top-level run.""" # ── Translated payload (the actual work the translator does) ──────── messages: list[TResponseInputItem] - """ - Responses-API input list. Feed straight into - ``Runner.run_streamed(input=...)``. Built from - :attr:`ag_ui.core.RunAgentInput.messages`. - - Validation is skipped to avoid Pydantic forward-ref resolution issues - with the agents SDK's internal types. - """ + """Responses-API input items, ready to pass to Runner.run*(input=...). + Validation is skipped here because the SDK's own input types use forward + refs Pydantic can't resolve from this module.""" tools: SkipValidation[list[FunctionTool]] = [] - """ - SDK :class:`FunctionTool` proxies for the AG-UI client tools. Merge with - the SDK agent's static tools before running. Built from - :attr:`ag_ui.core.RunAgentInput.tools`. - - Populated by the facade translators' ``to_sdk``; the engine's - ``AGUIToSDKTranslator.translate`` leaves it empty pending the - tools-wiring review fix (call ``translate_tools`` when using the engine - directly). - - Validation is skipped to avoid Pydantic forward-ref resolution issues - with the agents SDK's internal types. - """ + """FunctionTool proxies for the client's tools. Merge these with your + agent's own tools before running. The public translators fill this in; if + you call the inbound engine directly, call translate_tools yourself. + Validation skipped for the same forward-ref reason as messages.""" # ── Passthroughs (raw from the wire) ──────────────────────────────── state: Any - """ - The AG-UI user state as sent by the client. Typed ``Any`` because AG-UI - itself doesn't constrain its shape. - """ + """Whatever the client sent as state. Any, since AG-UI doesn't pin a shape.""" context: list[Context] - """ - Ambient context items (CopilotKit's ``useCopilotReadable`` etc.) — each - a ``{description, value}`` pair. Not auto-folded anywhere; use - :meth:`AGUIToSDKTranslator.translate_context` to render for the model. - """ + """Ambient {description, value} items from the frontend. Nothing folds these + into the prompt for you — run them through translate_context if you want the + model to see them.""" forwarded_props: Any - """ - Catch-all client-supplied props (model overrides, temperature, debug - flags, anything the client doesn't have a typed field for). - """ + """Grab-bag of extra client props (model overrides, temperature, flags, ...) + that don't have a dedicated field.""" resume: list[Any] | None = None - """ - HITL resume entries from the client — each ``{interruptId, status, - payload?}``. Only present when continuing from a previous interrupt. - - The Python AG-UI SDK does not expose this field yet; the translator reads - it defensively so the bundle is forward-compatible with the wire protocol. - """ + """Resume entries when continuing from an interrupt, else None. Read + defensively since not every RunAgentInput version carries this field yet.""" - # FunctionTool is not a Pydantic model, so we need to opt in. + # FunctionTool isn't a Pydantic model, so Pydantic won't accept it unless + # we explicitly allow arbitrary types. model_config = ConfigDict(arbitrary_types_allowed=True) -# FunctionTool's dataclass schema references the SDK-internal ``AgentBase`` -# forward ref; import it into scope and rebuild so ``tools`` resolves. +# FunctionTool's schema points at AgentBase, which Pydantic can't see from +# here. Pull it into scope and rebuild the model so the `tools` field resolves. from agents.agent import AgentBase # noqa: E402,F401 TranslatedInput.model_rebuild() diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py index 63da85d345..d14e96503e 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py @@ -1,23 +1,10 @@ -""" -Non-streaming facade. - -:class:`AGUINonStreamingTranslator` pairs with ``Runner.run`` / -``Runner.run_sync`` and exposes exactly two public methods, one per -direction: - - ``to_sdk(run_input)`` AG-UI ``RunAgentInput`` → SDK-ready bundle - ``to_agui(result)`` finished SDK run → complete AG-UI event list +"""Non-streaming translator. -All translation logic lives in the engine layer (:mod:`.engine`); this -class only orchestrates it. Stateless and -reusable — the engine instance a call needs is created inside it. Pass -engine subclasses via ``inbound_cls`` / ``outbound_cls`` to customize one -mapping without forking (design rule 4). - -The output is still a valid AG-UI event sequence — "non-streaming" only -means it is produced in one shot after the run finishes, so there is no -token-level text and no mid-tool ``STATE_DELTA``. For live output use the -main :class:`~ag_ui_openai_agents.AGUITranslator`. +AGUINonStreamingTranslator pairs with Runner.run / Runner.run_sync, +exposing to_sdk(run_input) and to_agui(result). Stateless and reusable. +Output is still a valid AG-UI event sequence, just produced in one shot +after the run finishes (no token-level text, no mid-tool STATE_DELTA). +For live output use the main AGUITranslator. """ from __future__ import annotations @@ -33,11 +20,9 @@ class only orchestrates it. Stateless and class AGUINonStreamingTranslator: - """ - Pairs with ``Runner.run`` / ``Runner.run_sync``. - - :: + """Pairs with Runner.run / Runner.run_sync. + Example: translator = AGUINonStreamingTranslator() bundle = translator.to_sdk(run_input) result = await Runner.run(agent, input=bundle.messages, ...) @@ -54,11 +39,17 @@ def __init__( self._outbound_cls = outbound_cls def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: - """AG-UI ``RunAgentInput`` → SDK-ready bundle (items, tools, state...). + """Translate an AG-UI RunAgentInput into an SDK-ready bundle. + + The bundle's tools field holds agents.FunctionTool proxies for the + client-declared tools — merge them with the agent's static tools + (agent.clone(tools=[*agent.tools, *bundle.tools])). + + Args: + run_input: The incoming AG-UI RunAgentInput. - The bundle's ``tools`` holds :class:`agents.FunctionTool` proxies for - the client-declared tools — merge them with the agent's static tools - (``agent.clone(tools=[*agent.tools, *bundle.tools])``). + Returns: + TranslatedInput with items, tools, and passthrough state. """ bundle = self._inbound.translate(run_input) if run_input.tools: @@ -68,11 +59,18 @@ def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: return bundle def to_agui(self, result: Any) -> list[BaseEvent]: - """Finished SDK run → complete AG-UI event sequence. + """Translate a finished SDK run into a complete AG-UI event sequence. - Accepts a ``RunResult`` (its ``new_items`` are read) or a plain - ``list[RunItem]``. Every item emits full triplets — no windows stay + Accepts a RunResult (its new_items are read) or a plain + list[RunItem]. Every item emits full triplets — no windows stay open, so there is nothing to finalize. + + Args: + result: A RunResult from Runner.run/run_sync, or a list of + RunItem. + + Returns: + The complete list of AG-UI BaseEvent instances for the run. """ items: list[RunItem] = ( result if isinstance(result, list) else list(result.new_items) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 506de4e61a..371a16d0cf 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -1,28 +1,12 @@ -""" -Streaming facade — the package's main translator. - -:class:`AGUITranslator` pairs with ``Runner.run_streamed`` and exposes -exactly two public methods, one per direction: - - ``to_sdk(run_input)`` AG-UI ``RunAgentInput`` → SDK-ready bundle - ``to_agui(stream_events)`` SDK event stream → live AG-UI event stream +"""Streaming translator — the package's main API. -All translation logic lives in the engine layer (:mod:`.engine`); this -class only orchestrates it. Window bookkeeping -and the final flush happen inside the ``to_agui`` iterator — callers never -see ``finalize``. +AGUITranslator pairs with Runner.run_streamed, exposing only +to_sdk(run_input) and to_agui(stream_events). Stateless and reusable — +each to_agui call creates the fresh stateful engine that run needs. +Lifecycle events (RUN_STARTED / RUN_FINISHED / RUN_ERROR) and session +persistence are the caller's job, not the translator's. -The facade is stateless and reusable: each ``to_agui`` call creates the -fresh stateful engine that run needs. Pass engine subclasses via -``inbound_cls`` / ``outbound_cls`` to customize one mapping without forking -(design rule 4). - -Lifecycle events (``RUN_STARTED``/``RUN_FINISHED``/``RUN_ERROR``), -``STATE_SNAPSHOT`` and session persistence stay in the caller's run loop -(design rule 2) — see ``examples/server.py``. - -Non-streaming runs (``Runner.run`` / ``run_sync``): use -:class:`~ag_ui_openai_agents.AGUINonStreamingTranslator`. +Non-streaming runs (Runner.run / run_sync): use AGUINonStreamingTranslator. """ from __future__ import annotations @@ -37,11 +21,9 @@ class only orchestrates it. Window bookkeeping class AGUITranslator: - """ - Main translator — pairs with ``Runner.run_streamed``. - - :: + """Main translator — pairs with Runner.run_streamed. + Example: translator = AGUITranslator() bundle = translator.to_sdk(run_input) result = Runner.run_streamed(agent, input=bundle.messages, ...) @@ -59,11 +41,17 @@ def __init__( self._outbound_cls = outbound_cls def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: - """AG-UI ``RunAgentInput`` → SDK-ready bundle (items, tools, state...). + """Translate an AG-UI RunAgentInput into an SDK-ready bundle. + + The bundle's tools field holds agents.FunctionTool proxies for the + client-declared tools — merge them with the agent's static tools + (agent.clone(tools=[*agent.tools, *bundle.tools])). + + Args: + run_input: The incoming AG-UI RunAgentInput. - The bundle's ``tools`` holds :class:`agents.FunctionTool` proxies for - the client-declared tools — merge them with the agent's static tools - (``agent.clone(tools=[*agent.tools, *bundle.tools])``). + Returns: + TranslatedInput with items, tools, and passthrough state. """ bundle = self._inbound.translate(run_input) if run_input.tools: @@ -73,12 +61,18 @@ def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: return bundle async def to_agui(self, stream_events: AsyncIterable[Any]) -> AsyncIterator[BaseEvent]: - """SDK event stream → live AG-UI event stream. + """Translate an SDK event stream into a live AG-UI event stream. + + Feed result.stream_events() from Runner.run_streamed. A fresh + stateful engine handles this run's windows; when the stream ends + the engine flush runs automatically — any still-open text / + tool-call / reasoning window is closed before the iterator finishes. + + Args: + stream_events: The SDK's async stream_events() iterator. - Feed ``result.stream_events()`` from ``Runner.run_streamed``. A fresh - stateful engine handles this run's windows; when the stream ends the - engine flush runs automatically — any still-open text / tool-call / - reasoning window is closed before the iterator finishes. + Yields: + AG-UI BaseEvent instances, ready to encode. """ outbound = self._outbound_cls() async for sdk_event in stream_events: diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index 6e9240853b..c0b3680a21 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -1,11 +1,11 @@ """ -Tests for the two-method facade translators. +Tests for the two-method translator APIs. -Covers the facade contract only — engine mappings have their own coverage: +Covers the public translator contract only — engine mappings have their own coverage: - ``to_sdk`` delegates to the inbound engine and populates ``tools``. - ``AGUITranslator.to_agui`` streams engine output live and appends the - engine flush, with a fresh engine per call (reusable facade). + engine flush, with a fresh engine per call (reusable translator). - ``AGUINonStreamingTranslator.to_agui`` accepts a ``RunResult``-shaped object or a plain item list. """ From 45767d1e9d916fae9591129f7cf6969a953a65f2 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 7 Jul 2026 03:05:49 +0200 Subject: [PATCH 07/94] feat(translator): update to_agui method to accept RunResultStreaming and improve README with streaming examples --- integrations/openai-agents/python/README.md | 250 ++++++++++++++---- .../non_streaming_translator.py | 5 +- .../src/ag_ui_openai_agents/translator.py | 28 +- .../python/tests/test_translators.py | 19 +- 4 files changed, 239 insertions(+), 63 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 15956a2fa5..d6042a7238 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -5,15 +5,29 @@ with the [AG-UI Protocol](https://github.com/ag-ui-protocol/ag-ui). Build your agent with the OpenAI SDK as usual, then translate its execution into AG-UI events any AG-UI client (e.g. CopilotKit) can render live. -The integration is a pair of **translators** — it converts data in both -directions and stays out of your way. You own the agent, the run loop, and -the transport; the translators only map shapes: +The integration is a pair of **translators**. You keep using the OpenAI Agents +SDK normally; the translators only map data at the AG-UI boundary: ``` -AG-UI RunAgentInput ──to_sdk()──▶ SDK input items + tools -SDK stream events ──to_agui()─▶ AG-UI BaseEvents +AG-UI RunAgentInput ──to_sdk()──▶ SDK input items + tools +SDK streamed result ──to_agui()─▶ AG-UI BaseEvents ``` +The recommended streaming flow is: + +```python +translator = AGUITranslator() + +bundle = translator.to_sdk(run_input) +result = Runner.run_streamed(agent, input=bundle.messages) + +async for event in translator.to_agui(result): + ... +``` + +`to_agui(result)` also accepts `result.stream_events()` if your code already +has the SDK event iterator. + ## Install ```bash @@ -26,9 +40,11 @@ For local development this package uses [uv](https://docs.astral.sh/uv/): uv sync ``` -## Quick start +## Quick Start: Streaming Endpoint -A complete SSE endpoint — agent, FastAPI, and the streaming translator: +This is the common server shape for AG-UI clients: accept `RunAgentInput`, run +your OpenAI agent with `Runner.run_streamed`, and stream AG-UI events back over +SSE. ```python from agents import Agent, Runner @@ -36,6 +52,7 @@ from ag_ui.core import ( EventType, RunAgentInput, RunErrorEvent, RunFinishedEvent, RunStartedEvent, ) +from ag_ui.encoder import EventEncoder from fastapi import FastAPI from fastapi.responses import StreamingResponse @@ -43,44 +60,43 @@ from ag_ui_openai_agents import AGUITranslator agent = Agent(name="assistant", instructions="Be concise.") translator = AGUITranslator() # stateless — one instance serves every request +encoder = EventEncoder() # AG-UI's own SSE encoder app = FastAPI() @app.post("/") async def run(body: RunAgentInput) -> StreamingResponse: - return StreamingResponse(_stream(body), media_type="text/event-stream") + return StreamingResponse(_stream(body), media_type=encoder.get_content_type()) async def _stream(body: RunAgentInput): - # 1 — AG-UI input → SDK-ready bundle (messages, tool proxies, passthroughs) + # 1. AG-UI input -> SDK-ready bundle. bundle = translator.to_sdk(body) - # Client-declared (frontend) tools arrive on the wire — merge per request. + # 2. Merge client-declared tools per request. run_agent = agent if bundle.tools: run_agent = agent.clone(tools=[*agent.tools, *bundle.tools]) - # 2 — lifecycle events are yours, not the translator's - yield _sse(RunStartedEvent( + # 3. Lifecycle events are emitted by your run loop. + yield encoder.encode(RunStartedEvent( type=EventType.RUN_STARTED, thread_id=body.thread_id, run_id=body.run_id, )) try: - # 3 — run the SDK agent; stream translated AG-UI events + # 4. Run the SDK agent normally, then translate the streamed result. result = Runner.run_streamed(run_agent, input=bundle.messages) - async for event in translator.to_agui(result.stream_events()): - yield _sse(event) + async for event in translator.to_agui(result): + yield encoder.encode(event) except Exception: - yield _sse(RunErrorEvent(type=EventType.RUN_ERROR, message="Agent run failed.")) + yield encoder.encode( + RunErrorEvent(type=EventType.RUN_ERROR, message="Agent run failed.") + ) return - yield _sse(RunFinishedEvent( + yield encoder.encode(RunFinishedEvent( type=EventType.RUN_FINISHED, thread_id=body.thread_id, run_id=body.run_id, )) - - -def _sse(event) -> bytes: - return f"data: {event.model_dump_json(by_alias=True, exclude_none=True)}\n\n".encode() ``` Test it: @@ -95,16 +111,15 @@ curl -N -X POST http://localhost:8000 \ }' ``` -Expected: `RUN_STARTED → TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT (×N) → -TEXT_MESSAGE_END → RUN_FINISHED`. +Expected: `RUN_STARTED -> TEXT_MESSAGE_START -> TEXT_MESSAGE_CONTENT (xN) -> +TEXT_MESSAGE_END -> RUN_FINISHED`. A full multi-demo server (chat, backend tools, human-in-the-loop, handoffs, orchestrator) lives in [`examples/`](examples/). ## Public API -Two facade translators, **two methods each** — everything else is theirs to -orchestrate: +There are two public translators, one for each OpenAI Agents SDK run mode: | Class | Pairs with | `to_agui(...)` returns | |---|---|---| @@ -114,33 +129,81 @@ orchestrate: Both are stateless and reusable — each `to_agui` call internally creates the fresh per-run engine it needs. Create one instance and share it. -### Streaming (the default) +### Choose a Pattern + +| Need | Use | +|---|---| +| Live chat, tool-call progress, reasoning progress | `AGUITranslator` with `Runner.run_streamed` | +| A finished event list after the model completes | `AGUINonStreamingTranslator` with `Runner.run` or `run_sync` | +| FastAPI SSE | Return `StreamingResponse(_stream(...), media_type="text/event-stream")` | +| WebSocket or another async transport | Iterate `translator.to_agui(result)` and send each event JSON | +| Custom model settings, tracing, guardrails, handoffs | Pass normal OpenAI Agents SDK args to `Runner.run*` | +| Custom AG-UI mapping behavior | Subclass an engine translator and inject it into the public translator | + +### Streaming: Live AG-UI Output AG-UI is an ordered event stream by design, so streaming is the primary mode: ```python translator = AGUITranslator() + bundle = translator.to_sdk(run_input) result = Runner.run_streamed(agent, input=bundle.messages) -async for event in translator.to_agui(result.stream_events()): + +async for event in translator.to_agui(result): ... # AG-UI BaseEvent, ready to encode ``` -`to_agui` folds all window bookkeeping in: any still-open text / tool-call / -reasoning window is closed automatically when the stream ends. +You may also pass the SDK event iterator directly: + +```python +async for event in translator.to_agui(result.stream_events()): + ... +``` + +`to_agui` handles the streaming bookkeeping for the run. If the SDK stream ends +while text, tool-call arguments, or reasoning output is still open, the +translator emits the matching close event before the iterator finishes. -### Non-streaming +All normal OpenAI Agents SDK run options stay on the SDK call: + +```python +result = Runner.run_streamed( + agent, + input=bundle.messages, + context=my_context, + max_turns=8, + run_config=run_config, +) + +async for event in translator.to_agui(result): + ... +``` + +### Non-Streaming: One-Shot AG-UI Output Same valid AG-UI event sequence, produced in one burst after the run finishes (no token-level deltas): ```python translator = AGUINonStreamingTranslator() + bundle = translator.to_sdk(run_input) result = await Runner.run(agent, input=bundle.messages) + events = translator.to_agui(result) # accepts RunResult or list[RunItem] ``` +Use this when your server or product does not need live tokens. For chat UIs, +the streaming translator is usually the better default. + +`Runner.run_sync` works the same way: + +```python +result = Runner.run_sync(agent, input=bundle.messages) +events = translator.to_agui(result) +``` + ### What `to_sdk` gives you `TranslatedInput` mirrors `RunAgentInput` field for field: @@ -152,12 +215,114 @@ events = translator.to_agui(result) # accepts RunResult or list[RunItem] | `state`, `context`, `forwarded_props` | passthrough — the library never injects them anywhere; render them into instructions/messages yourself if your app needs the model to see them | | `thread_id`, `run_id`, `parent_run_id`, `resume` | passthrough | -### Division of labor +The most important field is `bundle.messages`; pass it as `input=` to +`Runner.run_streamed`, `Runner.run`, or `Runner.run_sync`. + +If `bundle.tools` is non-empty, merge those tools into the agent for this +request: + +```python +run_agent = agent +if bundle.tools: + run_agent = agent.clone(tools=[*agent.tools, *bundle.tools]) +``` + +### What Your Code Still Owns -The translators translate; **your run loop orchestrates**. Lifecycle events -(`RUN_STARTED`/`RUN_FINISHED`/`RUN_ERROR`), `STATE_SNAPSHOT`, -`MESSAGES_SNAPSHOT`, session persistence, and transport (SSE/WebSocket) -stay in your code — see the quick start above for the minimal shape. +The translators translate. Your run loop still owns orchestration: + +| Concern | Owned by | +|---|---| +| `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` | Your server | +| `STATE_SNAPSHOT`, `MESSAGES_SNAPSHOT` | Your server, if your app needs them | +| Session storage and thread history | Your server | +| SSE, WebSocket, HTTP response shape | Your server/framework | +| OpenAI agent choice, model settings, handoffs, guardrails | Your OpenAI Agents SDK code | +| AG-UI message/tool/event shape conversion | This package | + +This keeps the integration framework-neutral. FastAPI, Starlette, Django, +aiohttp, raw ASGI, WebSockets, or tests can all use the same translator calls. + +### Transport Options + +For SSE, use the AG-UI SDK's own encoder — it produces one `data:` frame per +event and gives you the matching `Content-Type`: + +```python +from ag_ui.encoder import EventEncoder + +encoder = EventEncoder() + +async for event in translator.to_agui(result): + yield encoder.encode(event) +``` + +If you'd rather not depend on the encoder, the equivalent frame by hand is: + +```python +def encode_sse(event) -> bytes: + return f"data: {event.model_dump_json(by_alias=True, exclude_none=True)}\n\n".encode() +``` + +For WebSockets, send the same JSON payload: + +```python +async for event in translator.to_agui(result): + await websocket.send_text(event.model_dump_json(by_alias=True, exclude_none=True)) +``` + +For tests or in-process consumers, collect events directly: + +```python +events = [event async for event in translator.to_agui(result)] +``` + +### State, Context, and Forwarded Props + +`state`, `context`, and `forwarded_props` are preserved on `TranslatedInput`; +the translator does not automatically insert them into model instructions. +That is deliberate, because apps use these fields differently. + +One naming collision to be aware of — there are two unrelated "context"s: + +- AG-UI `RunAgentInput.context` — readable `{description, value}` items meant + for the **model** (prompt material). +- OpenAI Agents SDK `context=` on `Runner.run*` — a dependency-injection slot + for **tools and hooks**; the model never sees it. + +AG-UI's `context` field belongs in the prompt (example below); the SDK's +`context=` slot stays yours for whatever your tools need. + +```python +bundle = translator.to_sdk(run_input) + +instructions = agent.instructions +if bundle.context: + instructions += "\n\nContext:\n" + "\n".join( + f"- {item.description}: {item.value}" for item in bundle.context + ) + +run_agent = agent.clone(instructions=instructions) +result = Runner.run_streamed(run_agent, input=bundle.messages) +``` + +## Capabilities + +The streaming translator supports the OpenAI Agents SDK stream shapes AG-UI +clients care about: + +| Capability | AG-UI output | +|---|---| +| Assistant text | `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END` | +| Refusals | Text message events | +| Tool calls | `TOOL_CALL_START`, streamed `TOOL_CALL_ARGS`, `TOOL_CALL_END`, `TOOL_CALL_RESULT` | +| Reasoning text | `REASONING_START`, reasoning message events, `REASONING_END` | +| Encrypted reasoning replay data | `REASONING_ENCRYPTED_VALUE` | +| Hosted tools such as web search, file search, code interpreter | Tool call events | +| Handoffs and agents-as-tools | Tool call events plus step events | +| MCP approval requests | `CUSTOM` events | + +Unknown SDK event types are skipped with a debug log instead of crashing the run. ## Advanced: the engine layer @@ -205,21 +370,6 @@ The run stops before the proxy body executes; the frontend answers the tool call and sends the result back as a `tool` message in the next run's `messages`. See `examples/agents_examples/human_in_the_loop.py`. -## Supported AG-UI events - -- **Text**: `TEXT_MESSAGE_START` / `CONTENT` / `END` (token-level, refusals included) -- **Tool calls**: `TOOL_CALL_START` / `ARGS` / `END` / `RESULT` (args stream as deltas) -- **Reasoning**: `REASONING_START` / `MESSAGE_*` / `END`, plus - `REASONING_ENCRYPTED_VALUE` for replayable reasoning -- **Hosted tools** (web search, file search, code interpreter, ...): full - `TOOL_CALL_*` sequences -- **Handoffs & agents-as-tools**: translated as tool calls; `STEP_STARTED` / - `STEP_FINISHED` for multi-agent steps -- **MCP approval requests**: `CUSTOM` events - -Lifecycle (`RUN_*`) and snapshot events are emitted by your run loop, not -the translator (see division of labor above). - ## Gotchas - **Reasoning replay** (sending reasoning back to OpenAI) only works via diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py index d14e96503e..36269f6911 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py @@ -9,9 +9,8 @@ from __future__ import annotations -from typing import Any - from agents.items import RunItem +from agents.result import RunResult from ag_ui.core import BaseEvent, RunAgentInput from .engine.agui_to_sdk import AGUIToSDKTranslator @@ -58,7 +57,7 @@ def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: ) return bundle - def to_agui(self, result: Any) -> list[BaseEvent]: + def to_agui(self, result: RunResult | list[RunItem]) -> list[BaseEvent]: """Translate a finished SDK run into a complete AG-UI event sequence. Accepts a RunResult (its new_items are read) or a plain diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 371a16d0cf..90f7ff9863 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -1,7 +1,7 @@ """Streaming translator — the package's main API. -AGUITranslator pairs with Runner.run_streamed, exposing only -to_sdk(run_input) and to_agui(stream_events). Stateless and reusable — +AGUITranslator pairs with Runner.run_streamed, exposing +to_sdk(run_input) and to_agui(events). Stateless and reusable — each to_agui call creates the fresh stateful engine that run needs. Lifecycle events (RUN_STARTED / RUN_FINISHED / RUN_ERROR) and session persistence are the caller's job, not the translator's. @@ -11,8 +11,10 @@ from __future__ import annotations -from typing import Any, AsyncIterable, AsyncIterator +from typing import AsyncIterator +from agents.result import RunResultStreaming +from agents.stream_events import StreamEvent from ag_ui.core import BaseEvent, RunAgentInput from .engine.agui_to_sdk import AGUIToSDKTranslator @@ -27,7 +29,7 @@ class AGUITranslator: translator = AGUITranslator() bundle = translator.to_sdk(run_input) result = Runner.run_streamed(agent, input=bundle.messages, ...) - async for event in translator.to_agui(result.stream_events()): + async for event in translator.to_agui(result): ... # AG-UI BaseEvent, ready to encode """ @@ -60,20 +62,28 @@ def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: ) return bundle - async def to_agui(self, stream_events: AsyncIterable[Any]) -> AsyncIterator[BaseEvent]: + async def to_agui( + self, events: RunResultStreaming | AsyncIterator[StreamEvent] + ) -> AsyncIterator[BaseEvent]: """Translate an SDK event stream into a live AG-UI event stream. - Feed result.stream_events() from Runner.run_streamed. A fresh - stateful engine handles this run's windows; when the stream ends - the engine flush runs automatically — any still-open text / + Feed the result from Runner.run_streamed, or result.stream_events(). + A fresh stateful engine handles this run's windows; when the stream + ends the engine flush runs automatically — any still-open text / tool-call / reasoning window is closed before the iterator finishes. Args: - stream_events: The SDK's async stream_events() iterator. + events: The SDK RunResultStreaming object, or the async + iterator returned by its stream_events() method. Yields: AG-UI BaseEvent instances, ready to encode. """ + stream_events = ( + events.stream_events() + if isinstance(events, RunResultStreaming) + else events + ) outbound = self._outbound_cls() async for sdk_event in stream_events: for event in outbound.translate(sdk_event): diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index c0b3680a21..8e29cb4813 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -14,9 +14,11 @@ import asyncio from types import SimpleNamespace +from unittest.mock import MagicMock from ag_ui.core import CustomEvent, EventType, RunAgentInput, Tool, UserMessage from agents import FunctionTool +from agents.result import RunResultStreaming from ag_ui_openai_agents import AGUINonStreamingTranslator, AGUITranslator @@ -99,7 +101,7 @@ async def collect(): assert [e.name for e in events] == ["translated:a", "translated:b", "finalized"] -def test_streaming_facade_is_reusable_fresh_engine_per_run(): +def test_streaming_translator_is_reusable_fresh_engine_per_run(): translator = AGUITranslator(outbound_cls=_StubOutbound) before = _StubOutbound.instances @@ -111,6 +113,21 @@ async def one_run(): assert _StubOutbound.instances == before + 2 +def test_streaming_to_agui_accepts_run_streaming_result(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + + # spec= makes isinstance(result, RunResultStreaming) True, matching what + # to_agui actually checks — a bare duck-typed stand-in would not. + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream("sdk") + + async def collect(): + return [e async for e in translator.to_agui(result)] + + events = asyncio.run(collect()) + assert [e.name for e in events] == ["translated:sdk", "finalized"] + + # ── AGUINonStreamingTranslator.to_agui ─────────────────────────────────── From c118c1a37eda4e0cce975a1e6dabb8deaab8c52f Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 7 Jul 2026 03:25:11 +0200 Subject: [PATCH 08/94] refactor(openai-agents): support streaming-only architecture and delete non-streaming translator --- integrations/openai-agents/python/README.md | 50 +++--------- .../src/ag_ui_openai_agents/__init__.py | 8 +- .../ag_ui_openai_agents/engine/__init__.py | 5 +- .../ag_ui_openai_agents/engine/sdk_to_agui.py | 40 +++------- .../non_streaming_translator.py | 77 ------------------- .../src/ag_ui_openai_agents/translator.py | 2 - .../python/tests/test_translators.py | 23 +----- 7 files changed, 31 insertions(+), 174 deletions(-) delete mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index d6042a7238..6d9fade7c9 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -5,15 +5,15 @@ with the [AG-UI Protocol](https://github.com/ag-ui-protocol/ag-ui). Build your agent with the OpenAI SDK as usual, then translate its execution into AG-UI events any AG-UI client (e.g. CopilotKit) can render live. -The integration is a pair of **translators**. You keep using the OpenAI Agents -SDK normally; the translators only map data at the AG-UI boundary: +The integration is a **translator**. You keep using the OpenAI Agents +SDK normally; the translator only maps data at the AG-UI boundary: ``` AG-UI RunAgentInput ──to_sdk()──▶ SDK input items + tools SDK streamed result ──to_agui()─▶ AG-UI BaseEvents ``` -The recommended streaming flow is: +The flow is: ```python translator = AGUITranslator() @@ -119,14 +119,13 @@ orchestrator) lives in [`examples/`](examples/). ## Public API -There are two public translators, one for each OpenAI Agents SDK run mode: +There is one public translator; it pairs with the SDK's streaming run mode: | Class | Pairs with | `to_agui(...)` returns | |---|---|---| -| `AGUITranslator` (main) | `Runner.run_streamed` | async iterator of AG-UI events, live | -| `AGUINonStreamingTranslator` | `Runner.run` / `run_sync` | `list[BaseEvent]`, one shot | +| `AGUITranslator` | `Runner.run_streamed` | async iterator of AG-UI events, live | -Both are stateless and reusable — each `to_agui` call internally creates the +It is stateless and reusable — each `to_agui` call internally creates the fresh per-run engine it needs. Create one instance and share it. ### Choose a Pattern @@ -134,10 +133,9 @@ fresh per-run engine it needs. Create one instance and share it. | Need | Use | |---|---| | Live chat, tool-call progress, reasoning progress | `AGUITranslator` with `Runner.run_streamed` | -| A finished event list after the model completes | `AGUINonStreamingTranslator` with `Runner.run` or `run_sync` | | FastAPI SSE | Return `StreamingResponse(_stream(...), media_type="text/event-stream")` | | WebSocket or another async transport | Iterate `translator.to_agui(result)` and send each event JSON | -| Custom model settings, tracing, guardrails, handoffs | Pass normal OpenAI Agents SDK args to `Runner.run*` | +| Custom model settings, tracing, guardrails, handoffs | Pass normal OpenAI Agents SDK args to `Runner.run_streamed` | | Custom AG-UI mapping behavior | Subclass an engine translator and inject it into the public translator | ### Streaming: Live AG-UI Output @@ -180,43 +178,19 @@ async for event in translator.to_agui(result): ... ``` -### Non-Streaming: One-Shot AG-UI Output - -Same valid AG-UI event sequence, produced in one burst after the run -finishes (no token-level deltas): - -```python -translator = AGUINonStreamingTranslator() - -bundle = translator.to_sdk(run_input) -result = await Runner.run(agent, input=bundle.messages) - -events = translator.to_agui(result) # accepts RunResult or list[RunItem] -``` - -Use this when your server or product does not need live tokens. For chat UIs, -the streaming translator is usually the better default. - -`Runner.run_sync` works the same way: - -```python -result = Runner.run_sync(agent, input=bundle.messages) -events = translator.to_agui(result) -``` - ### What `to_sdk` gives you `TranslatedInput` mirrors `RunAgentInput` field for field: | AG-UI field | Lands in | |---|---| -| `messages` | `bundle.messages` — Responses-API input items for `Runner.run*` | +| `messages` | `bundle.messages` — Responses-API input items for `Runner.run_streamed` | | `tools` | `bundle.tools` — SDK `FunctionTool` proxies for client-declared tools; merge with `agent.clone(tools=[*agent.tools, *bundle.tools])` | | `state`, `context`, `forwarded_props` | passthrough — the library never injects them anywhere; render them into instructions/messages yourself if your app needs the model to see them | | `thread_id`, `run_id`, `parent_run_id`, `resume` | passthrough | The most important field is `bundle.messages`; pass it as `input=` to -`Runner.run_streamed`, `Runner.run`, or `Runner.run_sync`. +`Runner.run_streamed`. If `bundle.tools` is non-empty, merge those tools into the agent for this request: @@ -229,7 +203,7 @@ if bundle.tools: ### What Your Code Still Owns -The translators translate. Your run loop still owns orchestration: +The translator translates. Your run loop still owns orchestration: | Concern | Owned by | |---|---| @@ -326,7 +300,7 @@ Unknown SDK event types are skipped with a debug log instead of crashing the run ## Advanced: the engine layer -The public translators delegate to two independent, symmetric engine translators in +The public translator delegates to two independent, symmetric engine translators in `ag_ui_openai_agents.engine`: - `AGUIToSDKTranslator` — inbound; stateless, tiered per-type methods @@ -378,7 +352,7 @@ call and sends the result back as a `tool` message in the next run's run. Plaintext reasoning cannot be re-ingested and is dropped inbound. - The Responses API has no `tool` role — AG-UI `tool` messages become `function_call_output` items linked by `call_id`. -- Unknown/unsupported message and event types never crash the translators — +- Unknown/unsupported message and event types never crash the translator — they are dropped with a debug log. ## Testing diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py index d6f8036515..13d0c48cd5 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py @@ -1,8 +1,8 @@ """OpenAI Agents SDK × AG-UI Protocol integration. -Primary API — translators, one per run mode: +Primary API — the streaming translator: - from ag_ui_openai_agents import AGUITranslator, AGUINonStreamingTranslator + from ag_ui_openai_agents import AGUITranslator Advanced (per-mapping overrides) — the engine layer: @@ -17,15 +17,13 @@ SDKToAGUITranslator, TranslatedInput, ) -from .non_streaming_translator import AGUINonStreamingTranslator from .translator import AGUITranslator __version__ = "0.1.0" __all__ = [ - # Public translators (primary API — 2 methods each) + # Public translator (primary API — 2 methods) "AGUITranslator", - "AGUINonStreamingTranslator", # Engine translators (advanced / per-mapping overrides) "AGUIToSDKTranslator", "SDKToAGUITranslator", diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py index 63ea9ff75f..eb40f00c97 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py @@ -1,9 +1,8 @@ """Engine layer — the per-direction translation logic. Advanced use only: subclass an engine to customize a single mapping and -inject it into a public translator via inbound_cls / outbound_cls. For everything -else, import the translators from the package root instead (AGUITranslator, -AGUINonStreamingTranslator). +inject it into the public translator via inbound_cls / outbound_cls. For +everything else, import AGUITranslator from the package root instead. """ from __future__ import annotations diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py index 8a341b00ca..0796f3d0c5 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py @@ -82,10 +82,10 @@ class SDKToAGUITranslator: the first signal for a window arrives (output_item.added or, defensively, a bare delta), and *_END fires on the first close signal — output_item.done, the run-item commit, or finalize(), - whichever comes first. Run items double as the full emission path - for non-streaming runs: when no window was ever opened for an item, - its run-item translator emits the complete triplet from the - finished item. + whichever comes first. Run items double as a full emission path: + when no window was ever opened for an item (some backends never + stream raw deltas for it), its run-item translator emits the + complete triplet from the finished item. """ def __init__(self) -> None: @@ -97,7 +97,7 @@ def __init__(self) -> None: self._open_reasonings: dict[str, str] = {} # key -> phase message_id self._open_reasoning_parts: dict[str, str] = {} # key -> part message_id # When a run item shows up, we need to know if we already streamed and - # closed it (nothing to do) or if this is a non-streaming run and we + # closed it (nothing to do) or if no deltas ever arrived for it and we # have to emit it whole. These track what's been closed so we can tell. self._closed_text_ids: list[str] = [] self._closed_reasoning_ids: list[str] = [] @@ -166,24 +166,6 @@ def finalize(self) -> list[BaseEvent]: # TIER 2 — Bulk collection + per event family # ───────────────────────────────────────────────────────────────────── - def translate_items(self, items: list[RunItem]) -> list[BaseEvent]: - """Translate a finished run's items (non-streaming mode). - - Feed result.new_items from Runner.run / Runner.run_sync. Each - item emits its complete AG-UI sequence (full triplets — no - windows stay open), so no finalize() call is needed after. - - Args: - items: The run's finished RunItem list. - - Returns: - The complete list of AG-UI events for the run. - """ - events: list[BaseEvent] = [] - for item in items: - events.extend(self.translate_item(item)) - return events - def translate_raw_response_event(self, event: Any) -> list[BaseEvent]: """Handle a RawResponsesStreamEvent (token-level Responses deltas). @@ -452,9 +434,9 @@ def translate_reasoning_part_done(self, data: Any) -> list[BaseEvent]: def translate_item(self, item: RunItem) -> list[BaseEvent]: """Dispatch one SDK RunItem to the right per-type translator. - During streaming these act as safety-net closers (raw events - already streamed the content); for non-streaming runs they emit - the item's full AG-UI sequence. + Usually these act as safety-net closers (raw events already + streamed the content); when no deltas were ever streamed for the + item, they emit its full AG-UI sequence instead. Args: item: A finished SDK RunItem. @@ -487,7 +469,7 @@ def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEve """Handle an assistant message commit by closing its window or emitting the full triplet. Streamed: the raw deltas already carried the text — just close. - Non-streamed: emit TEXT_MESSAGE_START/CONTENT/END from the + No deltas seen: emit TEXT_MESSAGE_START/CONTENT/END from the finished item, using the SDK's own ItemHelpers extractors (text first, refusal as fallback — both are user-visible). @@ -793,8 +775,8 @@ def _reconcile( Returns: ("close", key) when a streamed window is still open, ("skip", None) when the item was already fully streamed and - closed, or ("new", None) when nothing was streamed - (non-streaming run) and the item must be emitted whole. + closed, or ("new", None) when nothing was streamed for the + item and it must be emitted whole. """ if self._is_real_id(raw_id): if raw_id in open_windows: diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py deleted file mode 100644 index 36269f6911..0000000000 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/non_streaming_translator.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Non-streaming translator. - -AGUINonStreamingTranslator pairs with Runner.run / Runner.run_sync, -exposing to_sdk(run_input) and to_agui(result). Stateless and reusable. -Output is still a valid AG-UI event sequence, just produced in one shot -after the run finishes (no token-level text, no mid-tool STATE_DELTA). -For live output use the main AGUITranslator. -""" - -from __future__ import annotations - -from agents.items import RunItem -from agents.result import RunResult -from ag_ui.core import BaseEvent, RunAgentInput - -from .engine.agui_to_sdk import AGUIToSDKTranslator -from .engine.sdk_to_agui import SDKToAGUITranslator -from .engine.types import TranslatedInput - - -class AGUINonStreamingTranslator: - """Pairs with Runner.run / Runner.run_sync. - - Example: - translator = AGUINonStreamingTranslator() - bundle = translator.to_sdk(run_input) - result = await Runner.run(agent, input=bundle.messages, ...) - events = translator.to_agui(result) # complete AG-UI sequence - """ - - def __init__( - self, - *, - inbound_cls: type[AGUIToSDKTranslator] = AGUIToSDKTranslator, - outbound_cls: type[SDKToAGUITranslator] = SDKToAGUITranslator, - ) -> None: - self._inbound = inbound_cls() - self._outbound_cls = outbound_cls - - def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: - """Translate an AG-UI RunAgentInput into an SDK-ready bundle. - - The bundle's tools field holds agents.FunctionTool proxies for the - client-declared tools — merge them with the agent's static tools - (agent.clone(tools=[*agent.tools, *bundle.tools])). - - Args: - run_input: The incoming AG-UI RunAgentInput. - - Returns: - TranslatedInput with items, tools, and passthrough state. - """ - bundle = self._inbound.translate(run_input) - if run_input.tools: - bundle = bundle.model_copy( - update={"tools": self._inbound.translate_tools(run_input.tools)} - ) - return bundle - - def to_agui(self, result: RunResult | list[RunItem]) -> list[BaseEvent]: - """Translate a finished SDK run into a complete AG-UI event sequence. - - Accepts a RunResult (its new_items are read) or a plain - list[RunItem]. Every item emits full triplets — no windows stay - open, so there is nothing to finalize. - - Args: - result: A RunResult from Runner.run/run_sync, or a list of - RunItem. - - Returns: - The complete list of AG-UI BaseEvent instances for the run. - """ - items: list[RunItem] = ( - result if isinstance(result, list) else list(result.new_items) - ) - return self._outbound_cls().translate_items(items) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 90f7ff9863..9ac26fab41 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -5,8 +5,6 @@ each to_agui call creates the fresh stateful engine that run needs. Lifecycle events (RUN_STARTED / RUN_FINISHED / RUN_ERROR) and session persistence are the caller's job, not the translator's. - -Non-streaming runs (Runner.run / run_sync): use AGUINonStreamingTranslator. """ from __future__ import annotations diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index 8e29cb4813..0dcfaf1f67 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -1,26 +1,23 @@ """ -Tests for the two-method translator APIs. +Tests for the two-method translator API. Covers the public translator contract only — engine mappings have their own coverage: - ``to_sdk`` delegates to the inbound engine and populates ``tools``. - ``AGUITranslator.to_agui`` streams engine output live and appends the engine flush, with a fresh engine per call (reusable translator). -- ``AGUINonStreamingTranslator.to_agui`` accepts a ``RunResult``-shaped - object or a plain item list. """ from __future__ import annotations import asyncio -from types import SimpleNamespace from unittest.mock import MagicMock from ag_ui.core import CustomEvent, EventType, RunAgentInput, Tool, UserMessage from agents import FunctionTool from agents.result import RunResultStreaming -from ag_ui_openai_agents import AGUINonStreamingTranslator, AGUITranslator +from ag_ui_openai_agents import AGUITranslator def _run_input(with_tool: bool = False) -> RunAgentInput: @@ -61,9 +58,6 @@ def translate(self, sdk_event): def finalize(self): return [_event("finalized")] - def translate_items(self, items): - return [_event(f"item:{item}") for item in items] - async def _fake_stream(*names: str): for name in names: @@ -84,7 +78,7 @@ def test_to_sdk_translates_messages_and_tools(): def test_to_sdk_without_tools_leaves_bundle_empty(): - bundle = AGUINonStreamingTranslator().to_sdk(_run_input()) + bundle = AGUITranslator().to_sdk(_run_input()) assert bundle.tools == [] @@ -126,14 +120,3 @@ async def collect(): events = asyncio.run(collect()) assert [e.name for e in events] == ["translated:sdk", "finalized"] - - -# ── AGUINonStreamingTranslator.to_agui ─────────────────────────────────── - - -def test_non_streaming_to_agui_accepts_run_result_and_list(): - translator = AGUINonStreamingTranslator(outbound_cls=_StubOutbound) - - result = SimpleNamespace(new_items=["i1", "i2"]) - assert [e.name for e in translator.to_agui(result)] == ["item:i1", "item:i2"] - assert [e.name for e in translator.to_agui(["i3"])] == ["item:i3"] From c2c4783e68d6beafd22a8dc05a770cb1b7a01908 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 8 Jul 2026 22:54:03 +0200 Subject: [PATCH 09/94] feat(translator): add MESSAGES_SNAPSHOT support to to_agui and enhance snapshot handling --- docs/integrations.mdx | 1 + integrations/openai-agents/python/README.md | 53 +++- .../ag_ui_openai_agents/engine/sdk_to_agui.py | 157 ++++++++-- .../src/ag_ui_openai_agents/translator.py | 46 ++- .../python/tests/test_snapshot.py | 274 ++++++++++++++++++ .../python/tests/test_translators.py | 92 +++++- 6 files changed, 595 insertions(+), 28 deletions(-) create mode 100644 integrations/openai-agents/python/tests/test_snapshot.py diff --git a/docs/integrations.mdx b/docs/integrations.mdx index 5eb3f18897..6365c66c59 100644 --- a/docs/integrations.mdx +++ b/docs/integrations.mdx @@ -23,6 +23,7 @@ that demonstrate the protocol's capabilities and versatility. - **[CrewAI](https://crewai.com)** - Streamline workflows across industries with powerful AI agents. - **[AG2](https://ag2.ai)** - The Open-Source AgentOS +- **[OpenAI Agents SDK](https://openai.github.io/openai-agents-python/)** - OpenAI's lightweight Python framework for building multi-agent workflows ## Infrastructure / Deployment diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 6d9fade7c9..d703e96d51 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -21,12 +21,13 @@ translator = AGUITranslator() bundle = translator.to_sdk(run_input) result = Runner.run_streamed(agent, input=bundle.messages) -async for event in translator.to_agui(result): +async for event in translator.to_agui(result, run_input=run_input): ... ``` `to_agui(result)` also accepts `result.stream_events()` if your code already -has the SDK event iterator. +has the SDK event iterator. The last event of the stream is a +`MESSAGES_SNAPSHOT` by default — see [Messages Snapshot](#messages-snapshot). ## Install @@ -85,8 +86,9 @@ async def _stream(body: RunAgentInput): try: # 4. Run the SDK agent normally, then translate the streamed result. + # to_agui appends a MESSAGES_SNAPSHOT by default (last event). result = Runner.run_streamed(run_agent, input=bundle.messages) - async for event in translator.to_agui(result): + async for event in translator.to_agui(result, run_input=body): yield encoder.encode(event) except Exception: yield encoder.encode( @@ -201,6 +203,48 @@ if bundle.tools: run_agent = agent.clone(tools=[*agent.tools, *bundle.tools]) ``` +### Messages Snapshot + +`MESSAGES_SNAPSHOT` lets the client resync its whole message list in one +event — it reconciles by message id, fixing anything the granular stream +couldn't express (a reload mid-conversation, history rewritten by a handoff +input filter, a dropped connection). `to_agui` appends one by default, as +its last event, right after the stream's own flush — pass `run_input` so it +has the prior turns; no `run_input`, no snapshot: + +```python +async for event in translator.to_agui(result, run_input=run_input): + yield encoder.encode(event) + +yield encoder.encode(RunFinishedEvent(...)) +``` + +The snapshot is `run_input.messages` (untouched, keeping the ids the client +already renders) plus this run's messages, built inline as the engine +streams — each message's id is resolved once and handed to both the +streamed event and the snapshot entry, so they can never disagree, even on +backends that don't stamp real ids (LiteLLM and similar). Reasoning items +are not included; the client keeps its streamed reasoning bubbles through +the merge on its own. If the run raises, the exception propagates out of +the `async for` before the snapshot line runs — nothing is emitted on the +error path. + +> `run_input.messages` passes through untouched — filter it first if it +> holds anything the client shouldn't see echoed back, e.g. a system +> prompt sent as history instead of via `agent.instructions`: +> ```python +> filtered = [m for m in run_input.messages if m.role != "system"] +> async for event in translator.to_agui(result, run_input=filtered): +> ... +> ``` + +Pass `emit_messages_snapshot=False` to opt out (e.g. you assemble your own). +Auto-emission works the same whether `to_agui` is given the +`RunResultStreaming` object or a bare `result.stream_events()` iterator — +the snapshot is built from the engine's own state (collected as it +streamed), not `result.new_items`, so there's nothing the bare-iterator +form is missing. + ### What Your Code Still Owns The translator translates. Your run loop still owns orchestration: @@ -208,7 +252,8 @@ The translator translates. Your run loop still owns orchestration: | Concern | Owned by | |---|---| | `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` | Your server | -| `STATE_SNAPSHOT`, `MESSAGES_SNAPSHOT` | Your server, if your app needs them | +| `STATE_SNAPSHOT` | Your server, if your app needs it | +| `MESSAGES_SNAPSHOT` | `to_agui` (on by default given `run_input`; pass `emit_messages_snapshot=False` to own it yourself) | | Session storage and thread history | Your server | | SSE, WebSocket, HTTP response shape | Your server/framework | | OpenAI agent choice, model settings, handoffs, guardrails | Your OpenAI Agents SDK code | diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py index 0796f3d0c5..05cb2ff04e 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py @@ -13,27 +13,34 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, Sequence from ag_ui.core import ( + AssistantMessage, BaseEvent, CustomEvent, EventType, + FunctionCall, + Message, + MessagesSnapshotEvent, ReasoningEncryptedValueEvent, ReasoningEndEvent, ReasoningMessageContentEvent, ReasoningMessageEndEvent, ReasoningMessageStartEvent, ReasoningStartEvent, + RunAgentInput, StepFinishedEvent, StepStartedEvent, TextMessageContentEvent, TextMessageEndEvent, TextMessageStartEvent, + ToolCall, ToolCallArgsEvent, ToolCallEndEvent, ToolCallResultEvent, ToolCallStartEvent, + ToolMessage, ) from agents import ( HandoffCallItem, @@ -108,6 +115,12 @@ def __init__(self) -> None: # the reasoning phase, and we still need the original id to attach it to. self._reasoning_phase_ids: dict[str, str] = {} self._current_step: str | None = None + # AG-UI Messages for the end-of-run MESSAGES_SNAPSHOT, built inline as + # each item resolves its id here — never a second pass over new_items, + # so a snapshot message's id can never diverge from its streamed one. + # Reasoning items are intentionally never appended (see + # translate_reasoning_item). + self._snapshot_messages: list[Message] = [] # ───────────────────────────────────────────────────────────────────── # TIER 1 — One-shot entry points @@ -162,6 +175,47 @@ def finalize(self) -> list[BaseEvent]: self._current_step = None return events + def build_messages_snapshot( + self, + run_input: RunAgentInput | Sequence[Message] | None = None, + ) -> MessagesSnapshotEvent: + """Build the end-of-run MESSAGES_SNAPSHOT event. + + The snapshot is the full conversation as the client should know + it: the run's prior messages passed through untouched (they keep + the ids the client already renders, so its id-keyed merge updates + in place instead of duplicating), followed by this run's messages + — collected here as the stream ran (see _record_* below), each + under the exact id its streamed event used. Same id, one + resolution: a snapshot message can never disagree with its + streamed counterpart, even on backends that stamp placeholder ids + (Chat Completions, LiteLLM) instead of real ones. + + Prior history comes from run_input.messages, not + result.to_input_list(): Responses-API input items carry no id slot + for user/system messages, so round-tripping prior turns through + them would mint fresh ids and duplicate every bubble on the + client. Filter run_input first if it carries anything the client + should not see echoed back (e.g. a system prompt sent as history). + + Args: + run_input: The run's RunAgentInput, a plain message list, or + None when there is no prior history to prepend. + + Returns: + A MESSAGES_SNAPSHOT event ready to encode. + """ + if run_input is None: + prior: list[Message] = [] + elif isinstance(run_input, RunAgentInput): + prior = list(run_input.messages or []) + else: + prior = list(run_input) + return MessagesSnapshotEvent( + type=EventType.MESSAGES_SNAPSHOT, + messages=[*prior, *self._snapshot_messages], + ) + # ───────────────────────────────────────────────────────────────────── # TIER 2 — Bulk collection + per event family # ───────────────────────────────────────────────────────────────────── @@ -481,12 +535,20 @@ def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEve """ raw = item.raw_item raw_id = read_attr(raw, "id") + text = ItemHelpers.extract_text(raw) or ItemHelpers.extract_refusal(raw) or "" action, key = self._reconcile(raw_id, self._open_texts, self._closed_text_ids) if action == "close": - return self._close_text(key) + # key is still open at this point — read its id before _close_text pops it. + message_id = self._open_texts[key] + events = self._close_text(key) + self._record_text(message_id, text) + return events if action == "skip": + # Already closed via a raw event before this commit arrived — + # no BaseEvents to emit, but the snapshot still needs this + # message, under the exact id that earlier close already used. + self._record_text(key, text) return [] - text = ItemHelpers.extract_text(raw) or ItemHelpers.extract_refusal(raw) or "" message_id = self._resolve_id(raw_id, new_message_id) events = self._open_text(message_id, message_id) if text: @@ -498,7 +560,9 @@ def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEve ) ) events.extend(self._close_text(message_id)) + self._record_text(message_id, text) return events + def translate_tool_call_item(self, item: ToolCallItem) -> list[BaseEvent]: """Handle a tool call commit by closing its window or emitting START/[ARGS]/END. @@ -519,11 +583,6 @@ def translate_tool_call_item(self, item: ToolCallItem) -> list[BaseEvent]: or read_attr(raw, "call_id") or self._resolve_id(read_attr(raw, "id"), new_tool_call_id) ) - for key, open_call_id in self._open_tool_calls.items(): - if open_call_id == call_id: - return self._close_tool_call(key) - if call_id in self._seen_call_ids: - return [] name = ( getattr(item, "tool_name", None) or read_attr(raw, "name") @@ -531,6 +590,16 @@ def translate_tool_call_item(self, item: ToolCallItem) -> list[BaseEvent]: or "" ) arguments = read_attr(raw, "arguments") or "" + for key, open_call_id in self._open_tool_calls.items(): + if open_call_id == call_id: + events = self._close_tool_call(key) + self._record_tool_call(call_id, name, arguments) + return events + if call_id in self._seen_call_ids: + # Already closed via a raw event before this commit arrived — + # no BaseEvents to emit, but the snapshot still needs this call. + self._record_tool_call(call_id, name, arguments) + return [] events = self._open_tool_call(call_id, call_id, name) if arguments: events.append( @@ -541,6 +610,7 @@ def translate_tool_call_item(self, item: ToolCallItem) -> list[BaseEvent]: ) ) events.extend(self._close_tool_call(call_id)) + self._record_tool_call(call_id, name, arguments) return events def translate_tool_call_output_item(self, item: ToolCallOutputItem) -> list[BaseEvent]: @@ -556,12 +626,14 @@ def translate_tool_call_output_item(self, item: ToolCallOutputItem) -> list[Base if not call_id: logger.debug("Tool output without call_id; skipping TOOL_CALL_RESULT") return [] + content = coerce_to_str(item.output) + result_id = self._record_result(call_id, content) return [ ToolCallResultEvent( type=EventType.TOOL_CALL_RESULT, - message_id=new_tool_result_id(call_id), + message_id=result_id, tool_call_id=call_id, - content=coerce_to_str(item.output), + content=content, ) ] @@ -638,12 +710,14 @@ def translate_handoff_output_item(self, item: HandoffOutputItem) -> list[BaseEve return [] target = read_attr(item.target_agent, "name") or "" output = read_attr(raw, "output") or f"Handed off to {target}" + content = coerce_to_str(output) + result_id = self._record_result(call_id, content) return [ ToolCallResultEvent( type=EventType.TOOL_CALL_RESULT, - message_id=new_tool_result_id(call_id), + message_id=result_id, tool_call_id=call_id, - content=coerce_to_str(output), + content=content, ) ] @@ -774,21 +848,25 @@ def _reconcile( Returns: ("close", key) when a streamed window is still open, - ("skip", None) when the item was already fully streamed and - closed, or ("new", None) when nothing was streamed for the - item and it must be emitted whole. + ("skip", resolved_id) when the item was already fully + streamed and closed — resolved_id is the id that streamed + close already used, so the caller can still record a + snapshot message under it (real id: raw_id itself, since + it's known directly; placeholder id: the queued closed id), + or ("new", None) when nothing was streamed for the item and + it must be emitted whole. """ if self._is_real_id(raw_id): if raw_id in open_windows: return ("close", raw_id) if raw_id in closed_ids: - return ("skip", None) + return ("skip", raw_id) return ("new", None) if open_windows: return ("close", next(iter(open_windows))) if closed_ids: - closed_ids.pop(0) - return ("skip", None) + resolved_id = closed_ids.pop(0) + return ("skip", resolved_id) return ("new", None) # ───────────────────────────────────────────────────────────────────── @@ -952,5 +1030,48 @@ def _emit_encrypted_value(self, key: str, item: Any) -> list[BaseEvent]: ) ] + # ───────────────────────────────────────────────────────────────────── + # TIER 4 — Internal helpers: snapshot message builders + # ───────────────────────────────────────────────────────────────────── + # + # One per streamed item, appended as the item commits — always with the + # id its streamed event already used, so build_messages_snapshot's output + # lines up with the stream. Reasoning is deliberately not recorded: the + # streamed reasoning bubbles survive the client merge on their own, and + # plaintext reasoning cannot be round-tripped back into an OpenAI run. + + def _record_text(self, message_id: str, text: str) -> None: + self._snapshot_messages.append( + AssistantMessage(id=message_id, role="assistant", content=text) + ) + + def _record_tool_call(self, call_id: str, name: str, arguments: str) -> None: + # The streamed TOOL_CALL_START carried no parent message id, so the + # client keyed the bubble by the tool call id — mirror that here. + self._snapshot_messages.append( + AssistantMessage( + id=call_id, + role="assistant", + tool_calls=[ + ToolCall( + id=call_id, + type="function", + function=FunctionCall(name=name, arguments=arguments), + ) + ], + ) + ) + + def _record_result(self, call_id: str, content: str) -> str: + # Returns the derived result id so the caller reuses the same value + # for its TOOL_CALL_RESULT event — snapshot and stream stay in sync. + result_id = new_tool_result_id(call_id) + self._snapshot_messages.append( + ToolMessage( + id=result_id, role="tool", tool_call_id=call_id, content=content + ) + ) + return result_id + __all__ = ["SDKToAGUITranslator"] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 9ac26fab41..fd8aeb6dfc 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -4,16 +4,17 @@ to_sdk(run_input) and to_agui(events). Stateless and reusable — each to_agui call creates the fresh stateful engine that run needs. Lifecycle events (RUN_STARTED / RUN_FINISHED / RUN_ERROR) and session -persistence are the caller's job, not the translator's. +persistence are the caller's job, not the translator's. MESSAGES_SNAPSHOT +is the one exception: to_agui appends it by default (see to_agui). """ from __future__ import annotations -from typing import AsyncIterator +from typing import AsyncIterator, Sequence from agents.result import RunResultStreaming from agents.stream_events import StreamEvent -from ag_ui.core import BaseEvent, RunAgentInput +from ag_ui.core import BaseEvent, Message, RunAgentInput from .engine.agui_to_sdk import AGUIToSDKTranslator from .engine.sdk_to_agui import SDKToAGUITranslator @@ -61,7 +62,11 @@ def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: return bundle async def to_agui( - self, events: RunResultStreaming | AsyncIterator[StreamEvent] + self, + events: RunResultStreaming | AsyncIterator[StreamEvent], + *, + run_input: RunAgentInput | Sequence[Message] | None = None, + emit_messages_snapshot: bool = True, ) -> AsyncIterator[BaseEvent]: """Translate an SDK event stream into a live AG-UI event stream. @@ -70,9 +75,40 @@ async def to_agui( ends the engine flush runs automatically — any still-open text / tool-call / reasoning window is closed before the iterator finishes. + By default the last event yielded is a MESSAGES_SNAPSHOT: run_input's + messages, untouched, plus this run's messages — collected by the + engine as it streamed (see SDKToAGUITranslator.build_messages_snapshot), + each under the same id its streamed event used, so the two can never + diverge. Requires both run_input and emit_messages_snapshot=True; + pass emit_messages_snapshot=False to opt out (e.g. you already emit + your own), or simply omit run_input if you have no prior history to + attach — either way no snapshot is appended. Works the same whether + events is the RunResultStreaming object or a bare stream_events() + iterator — the snapshot is built from engine state, not + result.new_items. + + Note: + run_input.messages passes through to the snapshot as-is. If it + carries messages your client should not see echoed back — a + system prompt sent as history instead of via + agent.instructions, for example — filter them out before + calling to_agui: + + filtered = [m for m in run_input.messages if m.role != "system"] + async for event in translator.to_agui( + result, run_input=filtered + ): + ... + Args: events: The SDK RunResultStreaming object, or the async iterator returned by its stream_events() method. + run_input: The RunAgentInput (or its messages) this run + started from, for the snapshot's prior-history half. + Required (together with emit_messages_snapshot=True) for + the snapshot to be appended. + emit_messages_snapshot: Whether to append a MESSAGES_SNAPSHOT + after the stream ends. Defaults to True. Yields: AG-UI BaseEvent instances, ready to encode. @@ -88,3 +124,5 @@ async def to_agui( yield event for event in outbound.finalize(): yield event + if run_input and emit_messages_snapshot: + yield outbound.build_messages_snapshot(run_input) diff --git a/integrations/openai-agents/python/tests/test_snapshot.py b/integrations/openai-agents/python/tests/test_snapshot.py new file mode 100644 index 0000000000..5238019d9b --- /dev/null +++ b/integrations/openai-agents/python/tests/test_snapshot.py @@ -0,0 +1,274 @@ +""" +Tests for SDKToAGUITranslator.build_messages_snapshot. + +The invariant that matters: every id in the snapshot equals the id the +streamed events already used — guaranteed by construction, since the +engine resolves each item's id once and hands it to both the streamed +event and the accumulated snapshot message. See the snapshot-message +builders in engine/sdk_to_agui.py. +""" + +from __future__ import annotations + +from agents import Agent +from agents.items import ( + MessageOutputItem, + ReasoningItem, + ToolCallItem, + ToolCallOutputItem, +) +from ag_ui.core import ( + AssistantMessage, + EventType, + RunAgentInput, + ToolMessage, + UserMessage, +) +from openai.types.responses import ( + ResponseFunctionToolCall, + ResponseOutputMessage, + ResponseOutputText, + ResponseReasoningItem, +) + +from ag_ui_openai_agents.engine import SDKToAGUITranslator + +_AGENT = Agent(name="test-agent") + + +def _text_item(item_id: str = "msg_abc", text: str = "hello") -> MessageOutputItem: + return MessageOutputItem( + agent=_AGENT, + raw_item=ResponseOutputMessage( + id=item_id, + type="message", + role="assistant", + status="completed", + content=[ + ResponseOutputText(type="output_text", text=text, annotations=[]) + ], + ), + ) + + +def _tool_call_item(call_id: str = "call_1", name: str = "get_weather") -> ToolCallItem: + return ToolCallItem( + agent=_AGENT, + raw_item=ResponseFunctionToolCall( + id="fc_1", + type="function_call", + call_id=call_id, + name=name, + arguments='{"city":"Cairo"}', + ), + ) + + +def _tool_output_item(call_id: str = "call_1", output: str = "sunny") -> ToolCallOutputItem: + return ToolCallOutputItem( + agent=_AGENT, + raw_item={ + "type": "function_call_output", + "call_id": call_id, + "output": output, + }, + output=output, + ) + + +def _reasoning_item(item_id: str = "rs_1") -> ReasoningItem: + return ReasoningItem( + agent=_AGENT, + raw_item=ResponseReasoningItem(id=item_id, type="reasoning", summary=[]), + ) + + +# ── id consistency: snapshot ids == streamed event ids ─────────────────── + + +def test_snapshot_ids_match_streamed_event_ids(): + engine = SDKToAGUITranslator() + items = [_text_item(), _tool_call_item(), _tool_output_item()] + streamed = [] + for item in items: + streamed.extend(engine.translate_item(item)) + streamed.extend(engine.finalize()) + + streamed_text_ids = { + e.message_id for e in streamed if e.type == EventType.TEXT_MESSAGE_START + } + streamed_call_ids = { + e.tool_call_id for e in streamed if e.type == EventType.TOOL_CALL_START + } + streamed_result_ids = { + e.message_id for e in streamed if e.type == EventType.TOOL_CALL_RESULT + } + + messages = engine._snapshot_messages + text_ids = { + m.id for m in messages + if isinstance(m, AssistantMessage) and not m.tool_calls + } + call_ids = { + call.id + for m in messages + if isinstance(m, AssistantMessage) and m.tool_calls + for call in m.tool_calls + } + result_ids = {m.id for m in messages if isinstance(m, ToolMessage)} + + assert text_ids == streamed_text_ids == {"msg_abc"} + assert call_ids == streamed_call_ids == {"call_1"} + assert result_ids == streamed_result_ids == {"call_1-result"} + + +# ── tool call + result round-trip ──────────────────────────────────────── + + +def test_tool_call_and_result_round_trip(): + engine = SDKToAGUITranslator() + engine.translate_item(_tool_call_item()) + engine.translate_item(_tool_output_item()) + + call_message, result_message = engine._snapshot_messages + assert isinstance(call_message, AssistantMessage) + assert call_message.id == "call_1" + assert call_message.tool_calls[0].function.name == "get_weather" + assert call_message.tool_calls[0].function.arguments == '{"city":"Cairo"}' + + assert isinstance(result_message, ToolMessage) + assert result_message.tool_call_id == "call_1" + assert result_message.id == "call_1-result" + assert result_message.content == "sunny" + + +# ── multi-turn history: prior messages pass through untouched ──────────── + + +def test_snapshot_prepends_input_history_with_original_ids(): + run_input = RunAgentInput( + thread_id="t1", + run_id="r1", + messages=[ + UserMessage(id="m1", role="user", content="hi"), + AssistantMessage(id="msg_prev", role="assistant", content="hello"), + UserMessage(id="m2", role="user", content="and again"), + ], + tools=[], + state={}, + context=[], + forwarded_props=None, + ) + engine = SDKToAGUITranslator() + engine.translate_item(_text_item("msg_new", "hi again")) + + event = engine.build_messages_snapshot(run_input) + + assert event.type == EventType.MESSAGES_SNAPSHOT + assert [m.id for m in event.messages] == ["m1", "msg_prev", "m2", "msg_new"] + # Prior messages are the same objects, not copies with fresh ids. + assert event.messages[0] is run_input.messages[0] + + +def test_snapshot_accepts_message_list_and_none_history(): + engine = SDKToAGUITranslator() + engine.translate_item(_text_item()) + + prior = [UserMessage(id="m1", role="user", content="hi")] + assert [m.id for m in engine.build_messages_snapshot(prior).messages] == [ + "m1", + "msg_abc", + ] + assert [m.id for m in engine.build_messages_snapshot(None).messages] == ["msg_abc"] + assert [m.id for m in engine.build_messages_snapshot().messages] == ["msg_abc"] + + +# ── graceful degradation ───────────────────────────────────────────────── + + +def test_reasoning_items_are_skipped_from_snapshot(): + engine = SDKToAGUITranslator() + engine.translate_item(_reasoning_item()) + engine.translate_item(_text_item()) + + assert [m.id for m in engine._snapshot_messages] == ["msg_abc"] + + +def test_unknown_item_types_are_skipped_not_raised(): + from types import SimpleNamespace + + engine = SDKToAGUITranslator() + unknown = SimpleNamespace(raw_item={"type": "something_else"}) + + events = engine.translate_item(unknown) + + assert events == [] + assert engine._snapshot_messages == [] + + +# ── raw close beats run-item commit: the real regression ──────────────── +# +# Some backends emit response.output_item.done (closing the window via the +# raw-event path) before the RunItemStreamEvent commit for the same item +# arrives. translate_message_output_item/translate_tool_call_item then hit +# their "skip" branch (nothing left to close) — that branch used to return +# [] without ever recording a snapshot message, so the item streamed fully +# but never made it into MESSAGES_SNAPSHOT. Observed on a real LiteLLM run. + + +def test_text_message_closed_by_raw_event_before_commit_still_snapshots(): + from types import SimpleNamespace + + engine = SDKToAGUITranslator() + + # response.output_item.added then .done — closes the window via the + # raw-event path, same as a real streaming run. + added = SimpleNamespace( + item=SimpleNamespace(type="message", id="msg_real"), output_index=0 + ) + done = SimpleNamespace( + item=SimpleNamespace(type="message", id="msg_real"), output_index=0 + ) + engine.translate_output_item_added(added) + engine.translate_output_item_done(done) + assert engine._snapshot_messages == [] # raw close alone never records + + # The run-item commit arrives after — must still record, under the + # same id the raw close already used. + engine.translate_item(_text_item("msg_real", "hello")) + + assert len(engine._snapshot_messages) == 1 + message = engine._snapshot_messages[0] + assert isinstance(message, AssistantMessage) + assert message.id == "msg_real" + assert message.content == "hello" + + +def test_tool_call_closed_by_raw_event_before_commit_still_snapshots(): + from types import SimpleNamespace + + engine = SDKToAGUITranslator() + + added = SimpleNamespace( + item=SimpleNamespace( + type="function_call", id="fc_1", call_id="call_real", name="get_weather" + ), + output_index=0, + ) + done = SimpleNamespace( + item=SimpleNamespace( + type="function_call", id="fc_1", call_id="call_real", name="get_weather" + ), + output_index=0, + ) + engine.translate_output_item_added(added) + engine.translate_output_item_done(done) + assert engine._snapshot_messages == [] + + engine.translate_item(_tool_call_item(call_id="call_real")) + + assert len(engine._snapshot_messages) == 1 + message = engine._snapshot_messages[0] + assert isinstance(message, AssistantMessage) + assert message.tool_calls[0].id == "call_real" + assert message.tool_calls[0].function.name == "get_weather" diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index 0dcfaf1f67..c3a160fd21 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -6,6 +6,11 @@ - ``to_sdk`` delegates to the inbound engine and populates ``tools``. - ``AGUITranslator.to_agui`` streams engine output live and appends the engine flush, with a fresh engine per call (reusable translator). +- ``to_agui`` appends a MESSAGES_SNAPSHOT by default; snapshot content + itself is covered in ``test_snapshot.py``, this file only checks the + wiring (default on given a ``run_input``, ``emit_messages_snapshot=False`` + opts out, no ``run_input`` means no snapshot — same for bare iterators, + since the snapshot no longer depends on ``result.new_items``). """ from __future__ import annotations @@ -13,7 +18,14 @@ import asyncio from unittest.mock import MagicMock -from ag_ui.core import CustomEvent, EventType, RunAgentInput, Tool, UserMessage +from ag_ui.core import ( + CustomEvent, + EventType, + MessagesSnapshotEvent, + RunAgentInput, + Tool, + UserMessage, +) from agents import FunctionTool from agents.result import RunResultStreaming @@ -51,6 +63,7 @@ class _StubOutbound: def __init__(self) -> None: type(self).instances += 1 + self._snapshot_messages: list = [] def translate(self, sdk_event): return [_event(f"translated:{sdk_event}")] @@ -58,6 +71,13 @@ def translate(self, sdk_event): def finalize(self): return [_event("finalized")] + def build_messages_snapshot(self, run_input=None): + prior = getattr(run_input, "messages", run_input) or [] + return MessagesSnapshotEvent( + type=EventType.MESSAGES_SNAPSHOT, + messages=[*prior, *self._snapshot_messages], + ) + async def _fake_stream(*names: str): for name in names: @@ -114,9 +134,77 @@ def test_streaming_to_agui_accepts_run_streaming_result(): # to_agui actually checks — a bare duck-typed stand-in would not. result = MagicMock(spec=RunResultStreaming) result.stream_events.return_value = _fake_stream("sdk") + result.new_items = [] + + async def collect(): + return [e async for e in translator.to_agui(result, run_input=_run_input())] + + events = asyncio.run(collect()) + assert [e.name for e in events[:2]] == ["translated:sdk", "finalized"] + assert isinstance(events[2], MessagesSnapshotEvent) + + +# ── AGUITranslator.to_agui (MESSAGES_SNAPSHOT) ─────────────────────────── + + +def test_to_agui_emits_snapshot_by_default(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream() + result.new_items = [] + run_input = _run_input() + + async def collect(): + return [e async for e in translator.to_agui(result, run_input=run_input)] + + events = asyncio.run(collect()) + snapshot = events[-1] + assert isinstance(snapshot, MessagesSnapshotEvent) + assert [m.id for m in snapshot.messages] == ["m1"] + + +def test_to_agui_emit_messages_snapshot_false_opts_out(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream() + result.new_items = [] + + async def collect(): + return [ + e + async for e in translator.to_agui( + result, run_input=_run_input(), emit_messages_snapshot=False + ) + ] + + events = asyncio.run(collect()) + assert not any(isinstance(e, MessagesSnapshotEvent) for e in events) + + +def test_to_agui_skips_snapshot_without_run_input(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream() + result.new_items = [] async def collect(): return [e async for e in translator.to_agui(result)] events = asyncio.run(collect()) - assert [e.name for e in events] == ["translated:sdk", "finalized"] + assert not any(isinstance(e, MessagesSnapshotEvent) for e in events) + + +def test_to_agui_emits_snapshot_for_bare_iterator_too(): + # The snapshot is built from the engine's own accumulator, not + # result.new_items, so a bare stream_events() iterator works the same + # as passing the RunResultStreaming object itself. + translator = AGUITranslator(outbound_cls=_StubOutbound) + + async def collect(): + return [ + e + async for e in translator.to_agui(_fake_stream("a"), run_input=_run_input()) + ] + + events = asyncio.run(collect()) + assert isinstance(events[-1], MessagesSnapshotEvent) From 91e22a8847e5a5f842bca77e3565f2490a7b82cc Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Thu, 9 Jul 2026 12:03:31 +0200 Subject: [PATCH 10/94] feat(translator): update to_agui to always wrap streams with lifecycle events and append MESSAGES_SNAPSHOT --- integrations/openai-agents/python/README.md | 105 ++++++++------- .../src/ag_ui_openai_agents/translator.py | 102 +++++++++----- .../python/tests/test_translators.py | 124 ++++++++++++++---- 3 files changed, 220 insertions(+), 111 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index d703e96d51..9de99f7a5e 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -21,13 +21,16 @@ translator = AGUITranslator() bundle = translator.to_sdk(run_input) result = Runner.run_streamed(agent, input=bundle.messages) -async for event in translator.to_agui(result, run_input=run_input): +async for event in translator.to_agui(result, run_input): ... ``` `to_agui(result)` also accepts `result.stream_events()` if your code already -has the SDK event iterator. The last event of the stream is a -`MESSAGES_SNAPSHOT` by default — see [Messages Snapshot](#messages-snapshot). +has the SDK event iterator. The stream is always wrapped with `RUN_STARTED` +(first) and `RUN_FINISHED`/`RUN_ERROR` (last) — thread_id/run_id come from +`run_input`, or pass them explicitly. The event just before `RUN_FINISHED` +is a `MESSAGES_SNAPSHOT` by default — see +[Messages Snapshot](#messages-snapshot). ## Install @@ -49,10 +52,7 @@ SSE. ```python from agents import Agent, Runner -from ag_ui.core import ( - EventType, RunAgentInput, - RunErrorEvent, RunFinishedEvent, RunStartedEvent, -) +from ag_ui.core import RunAgentInput from ag_ui.encoder import EventEncoder from fastapi import FastAPI from fastapi.responses import StreamingResponse @@ -79,26 +79,12 @@ async def _stream(body: RunAgentInput): if bundle.tools: run_agent = agent.clone(tools=[*agent.tools, *bundle.tools]) - # 3. Lifecycle events are emitted by your run loop. - yield encoder.encode(RunStartedEvent( - type=EventType.RUN_STARTED, thread_id=body.thread_id, run_id=body.run_id, - )) - - try: - # 4. Run the SDK agent normally, then translate the streamed result. - # to_agui appends a MESSAGES_SNAPSHOT by default (last event). - result = Runner.run_streamed(run_agent, input=bundle.messages) - async for event in translator.to_agui(result, run_input=body): - yield encoder.encode(event) - except Exception: - yield encoder.encode( - RunErrorEvent(type=EventType.RUN_ERROR, message="Agent run failed.") - ) - return - - yield encoder.encode(RunFinishedEvent( - type=EventType.RUN_FINISHED, thread_id=body.thread_id, run_id=body.run_id, - )) + # 3. Run the SDK agent normally, then translate the streamed result. + # to_agui wraps it with RUN_STARTED/RUN_FINISHED/RUN_ERROR and + # appends a MESSAGES_SNAPSHOT by default (just before RUN_FINISHED). + result = Runner.run_streamed(run_agent, input=bundle.messages) + async for event in translator.to_agui(result, body): + yield encoder.encode(event) ``` Test it: @@ -136,7 +122,7 @@ fresh per-run engine it needs. Create one instance and share it. |---|---| | Live chat, tool-call progress, reasoning progress | `AGUITranslator` with `Runner.run_streamed` | | FastAPI SSE | Return `StreamingResponse(_stream(...), media_type="text/event-stream")` | -| WebSocket or another async transport | Iterate `translator.to_agui(result)` and send each event JSON | +| WebSocket or another async transport | Iterate `translator.to_agui(result, run_input)` and send each event JSON | | Custom model settings, tracing, guardrails, handoffs | Pass normal OpenAI Agents SDK args to `Runner.run_streamed` | | Custom AG-UI mapping behavior | Subclass an engine translator and inject it into the public translator | @@ -150,20 +136,22 @@ translator = AGUITranslator() bundle = translator.to_sdk(run_input) result = Runner.run_streamed(agent, input=bundle.messages) -async for event in translator.to_agui(result): +async for event in translator.to_agui(result, run_input): ... # AG-UI BaseEvent, ready to encode ``` You may also pass the SDK event iterator directly: ```python -async for event in translator.to_agui(result.stream_events()): +async for event in translator.to_agui(result.stream_events(), run_input): ... ``` `to_agui` handles the streaming bookkeeping for the run. If the SDK stream ends while text, tool-call arguments, or reasoning output is still open, the -translator emits the matching close event before the iterator finishes. +translator emits the matching close event before the iterator finishes. It +always wraps the whole stream with `RUN_STARTED` (first) and +`RUN_FINISHED`/`RUN_ERROR` (last) — see [Lifecycle Events](#lifecycle-events). All normal OpenAI Agents SDK run options stay on the SDK call: @@ -176,7 +164,7 @@ result = Runner.run_streamed( run_config=run_config, ) -async for event in translator.to_agui(result): +async for event in translator.to_agui(result, run_input): ... ``` @@ -203,20 +191,39 @@ if bundle.tools: run_agent = agent.clone(tools=[*agent.tools, *bundle.tools]) ``` +### Lifecycle Events + +`to_agui` always wraps the stream: `RUN_STARTED` is the first event +yielded, and `RUN_FINISHED` is the last — or `RUN_ERROR` if the stream +raises, in which case the exception is re-raised after that event so your +own logging/observability still sees it. This covers `asyncio.CancelledError` +too (a mid-stream timeout or dropped connection), not just ordinary +exceptions — it's `BaseException`, not `Exception`, so a plain +`except Exception` would miss it and the client would just see the +stream stop with no `RUN_ERROR` and no `RUN_FINISHED`. Not optional — +every caller needs these three events, so there's no flag to turn them off: + +```python +async for event in translator.to_agui(result, run_input): + yield encoder.encode(event) +``` + +`thread_id`/`run_id` come straight off `run_input` — no separate params to +pass, since `run_input` is already required for the lifecycle events (and, +by default, the snapshot). + ### Messages Snapshot `MESSAGES_SNAPSHOT` lets the client resync its whole message list in one event — it reconciles by message id, fixing anything the granular stream couldn't express (a reload mid-conversation, history rewritten by a handoff -input filter, a dropped connection). `to_agui` appends one by default, as -its last event, right after the stream's own flush — pass `run_input` so it -has the prior turns; no `run_input`, no snapshot: +input filter, a dropped connection). `to_agui` appends one by default, +right after the stream's own flush and just before `RUN_FINISHED`, using +`run_input.messages` for the prior turns: ```python -async for event in translator.to_agui(result, run_input=run_input): +async for event in translator.to_agui(result, run_input): yield encoder.encode(event) - -yield encoder.encode(RunFinishedEvent(...)) ``` The snapshot is `run_input.messages` (untouched, keeping the ids the client @@ -225,16 +232,18 @@ streams — each message's id is resolved once and handed to both the streamed event and the snapshot entry, so they can never disagree, even on backends that don't stamp real ids (LiteLLM and similar). Reasoning items are not included; the client keeps its streamed reasoning bubbles through -the merge on its own. If the run raises, the exception propagates out of -the `async for` before the snapshot line runs — nothing is emitted on the -error path. +the merge on its own. If the run raises, `to_agui` yields `RUN_ERROR` and +re-raises before the snapshot line runs — nothing is emitted on the error +path. > `run_input.messages` passes through untouched — filter it first if it > holds anything the client shouldn't see echoed back, e.g. a system > prompt sent as history instead of via `agent.instructions`: > ```python -> filtered = [m for m in run_input.messages if m.role != "system"] -> async for event in translator.to_agui(result, run_input=filtered): +> filtered = run_input.model_copy( +> update={"messages": [m for m in run_input.messages if m.role != "system"]} +> ) +> async for event in translator.to_agui(result, filtered): > ... > ``` @@ -251,9 +260,9 @@ The translator translates. Your run loop still owns orchestration: | Concern | Owned by | |---|---| -| `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` | Your server | +| `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` | `to_agui` (always) | | `STATE_SNAPSHOT` | Your server, if your app needs it | -| `MESSAGES_SNAPSHOT` | `to_agui` (on by default given `run_input`; pass `emit_messages_snapshot=False` to own it yourself) | +| `MESSAGES_SNAPSHOT` | `to_agui` (on by default; pass `emit_messages_snapshot=False` to own it yourself) | | Session storage and thread history | Your server | | SSE, WebSocket, HTTP response shape | Your server/framework | | OpenAI agent choice, model settings, handoffs, guardrails | Your OpenAI Agents SDK code | @@ -272,7 +281,7 @@ from ag_ui.encoder import EventEncoder encoder = EventEncoder() -async for event in translator.to_agui(result): +async for event in translator.to_agui(result, run_input): yield encoder.encode(event) ``` @@ -286,14 +295,14 @@ def encode_sse(event) -> bytes: For WebSockets, send the same JSON payload: ```python -async for event in translator.to_agui(result): +async for event in translator.to_agui(result, run_input): await websocket.send_text(event.model_dump_json(by_alias=True, exclude_none=True)) ``` For tests or in-process consumers, collect events directly: ```python -events = [event async for event in translator.to_agui(result)] +events = [event async for event in translator.to_agui(result, run_input)] ``` ### State, Context, and Forwarded Props diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index fd8aeb6dfc..3f4e44e13f 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -1,20 +1,29 @@ """Streaming translator — the package's main API. AGUITranslator pairs with Runner.run_streamed, exposing -to_sdk(run_input) and to_agui(events). Stateless and reusable — +to_sdk(run_input) and to_agui(events, run_input). Stateless and reusable — each to_agui call creates the fresh stateful engine that run needs. -Lifecycle events (RUN_STARTED / RUN_FINISHED / RUN_ERROR) and session -persistence are the caller's job, not the translator's. MESSAGES_SNAPSHOT -is the one exception: to_agui appends it by default (see to_agui). +to_agui always wraps the run with RUN_STARTED / RUN_FINISHED / RUN_ERROR — +not optional, every caller needs them, and thread_id/run_id come straight +off run_input. MESSAGES_SNAPSHOT is appended too, by default (see to_agui). +Session persistence is still the caller's job. """ from __future__ import annotations -from typing import AsyncIterator, Sequence +import asyncio +from typing import AsyncIterator from agents.result import RunResultStreaming from agents.stream_events import StreamEvent -from ag_ui.core import BaseEvent, Message, RunAgentInput +from ag_ui.core import ( + BaseEvent, + EventType, + RunAgentInput, + RunErrorEvent, + RunFinishedEvent, + RunStartedEvent, +) from .engine.agui_to_sdk import AGUIToSDKTranslator from .engine.sdk_to_agui import SDKToAGUITranslator @@ -28,7 +37,7 @@ class AGUITranslator: translator = AGUITranslator() bundle = translator.to_sdk(run_input) result = Runner.run_streamed(agent, input=bundle.messages, ...) - async for event in translator.to_agui(result): + async for event in translator.to_agui(result, run_input): ... # AG-UI BaseEvent, ready to encode """ @@ -64,8 +73,8 @@ def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: async def to_agui( self, events: RunResultStreaming | AsyncIterator[StreamEvent], + run_input: RunAgentInput, *, - run_input: RunAgentInput | Sequence[Message] | None = None, emit_messages_snapshot: bool = True, ) -> AsyncIterator[BaseEvent]: """Translate an SDK event stream into a live AG-UI event stream. @@ -75,17 +84,22 @@ async def to_agui( ends the engine flush runs automatically — any still-open text / tool-call / reasoning window is closed before the iterator finishes. - By default the last event yielded is a MESSAGES_SNAPSHOT: run_input's - messages, untouched, plus this run's messages — collected by the - engine as it streamed (see SDKToAGUITranslator.build_messages_snapshot), - each under the same id its streamed event used, so the two can never - diverge. Requires both run_input and emit_messages_snapshot=True; - pass emit_messages_snapshot=False to opt out (e.g. you already emit - your own), or simply omit run_input if you have no prior history to - attach — either way no snapshot is appended. Works the same whether - events is the RunResultStreaming object or a bare stream_events() - iterator — the snapshot is built from engine state, not - result.new_items. + The first event yielded is always RUN_STARTED and the last is + always RUN_FINISHED (or RUN_ERROR, if the stream raises — the + error event is yielded and then the exception re-raised; this + includes asyncio.CancelledError from a mid-stream timeout or + dropped connection, not just ordinary exceptions). Not optional. + thread_id/run_id come straight off run_input. + + Just before RUN_FINISHED, a MESSAGES_SNAPSHOT is appended by + default: run_input.messages, untouched, plus this run's messages — + collected by the engine as it streamed (see + SDKToAGUITranslator.build_messages_snapshot), each under the same + id its streamed event used, so the two can never diverge. Pass + emit_messages_snapshot=False to opt out (e.g. you assemble your + own). Works the same whether events is the RunResultStreaming + object or a bare stream_events() iterator — the snapshot is built + from engine state, not result.new_items. Note: run_input.messages passes through to the snapshot as-is. If it @@ -94,35 +108,55 @@ async def to_agui( agent.instructions, for example — filter them out before calling to_agui: - filtered = [m for m in run_input.messages if m.role != "system"] - async for event in translator.to_agui( - result, run_input=filtered - ): + filtered = run_input.model_copy( + update={"messages": [m for m in run_input.messages if m.role != "system"]} + ) + async for event in translator.to_agui(result, filtered): ... Args: events: The SDK RunResultStreaming object, or the async iterator returned by its stream_events() method. - run_input: The RunAgentInput (or its messages) this run - started from, for the snapshot's prior-history half. - Required (together with emit_messages_snapshot=True) for - the snapshot to be appended. + run_input: The RunAgentInput this run started from — supplies + thread_id/run_id for the lifecycle events and, unless + emit_messages_snapshot=False, the snapshot's prior-history + half. emit_messages_snapshot: Whether to append a MESSAGES_SNAPSHOT - after the stream ends. Defaults to True. + just before RUN_FINISHED. Defaults to True. Yields: AG-UI BaseEvent instances, ready to encode. """ + yield RunStartedEvent( + type=EventType.RUN_STARTED, + thread_id=run_input.thread_id, + run_id=run_input.run_id, + ) + stream_events = ( events.stream_events() if isinstance(events, RunResultStreaming) else events ) outbound = self._outbound_cls() - async for sdk_event in stream_events: - for event in outbound.translate(sdk_event): + try: + async for sdk_event in stream_events: + for event in outbound.translate(sdk_event): + yield event + for event in outbound.finalize(): yield event - for event in outbound.finalize(): - yield event - if run_input and emit_messages_snapshot: - yield outbound.build_messages_snapshot(run_input) + if emit_messages_snapshot: + yield outbound.build_messages_snapshot(run_input) + except (Exception, asyncio.CancelledError) as exc: + # asyncio.CancelledError is BaseException, not Exception (3.8+) — + # a mid-stream cancellation (timeout, dropped connection) would + # otherwise skip this handler entirely and the client would see + # the stream just stop, with no RUN_ERROR and no RUN_FINISHED. + yield RunErrorEvent(type=EventType.RUN_ERROR, message=str(exc)) + raise + + yield RunFinishedEvent( + type=EventType.RUN_FINISHED, + thread_id=run_input.thread_id, + run_id=run_input.run_id, + ) diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index c3a160fd21..9034c2ff19 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -6,11 +6,13 @@ - ``to_sdk`` delegates to the inbound engine and populates ``tools``. - ``AGUITranslator.to_agui`` streams engine output live and appends the engine flush, with a fresh engine per call (reusable translator). -- ``to_agui`` appends a MESSAGES_SNAPSHOT by default; snapshot content - itself is covered in ``test_snapshot.py``, this file only checks the - wiring (default on given a ``run_input``, ``emit_messages_snapshot=False`` - opts out, no ``run_input`` means no snapshot — same for bare iterators, - since the snapshot no longer depends on ``result.new_items``). +- ``to_agui`` always wraps the stream with RUN_STARTED / RUN_FINISHED / + RUN_ERROR — not optional, thread_id/run_id come straight off run_input. +- ``to_agui`` appends a MESSAGES_SNAPSHOT by default just before + RUN_FINISHED; snapshot content itself is covered in ``test_snapshot.py``, + this file only checks the wiring (default on, ``emit_messages_snapshot=False`` + opts out — same for bare iterators, since the snapshot no longer depends + on ``result.new_items``). """ from __future__ import annotations @@ -18,11 +20,15 @@ import asyncio from unittest.mock import MagicMock +import pytest from ag_ui.core import ( CustomEvent, EventType, MessagesSnapshotEvent, RunAgentInput, + RunErrorEvent, + RunFinishedEvent, + RunStartedEvent, Tool, UserMessage, ) @@ -109,10 +115,21 @@ def test_streaming_to_agui_streams_then_finalizes(): translator = AGUITranslator(outbound_cls=_StubOutbound) async def collect(): - return [e async for e in translator.to_agui(_fake_stream("a", "b"))] + return [ + e + async for e in translator.to_agui( + _fake_stream("a", "b"), _run_input(), emit_messages_snapshot=False + ) + ] events = asyncio.run(collect()) - assert [e.name for e in events] == ["translated:a", "translated:b", "finalized"] + assert isinstance(events[0], RunStartedEvent) + assert isinstance(events[-1], RunFinishedEvent) + assert [e.name for e in events[1:-1]] == [ + "translated:a", + "translated:b", + "finalized", + ] def test_streaming_translator_is_reusable_fresh_engine_per_run(): @@ -120,7 +137,7 @@ def test_streaming_translator_is_reusable_fresh_engine_per_run(): before = _StubOutbound.instances async def one_run(): - return [e async for e in translator.to_agui(_fake_stream("x"))] + return [e async for e in translator.to_agui(_fake_stream("x"), _run_input())] asyncio.run(one_run()) asyncio.run(one_run()) @@ -137,11 +154,13 @@ def test_streaming_to_agui_accepts_run_streaming_result(): result.new_items = [] async def collect(): - return [e async for e in translator.to_agui(result, run_input=_run_input())] + return [e async for e in translator.to_agui(result, _run_input())] events = asyncio.run(collect()) - assert [e.name for e in events[:2]] == ["translated:sdk", "finalized"] - assert isinstance(events[2], MessagesSnapshotEvent) + assert isinstance(events[0], RunStartedEvent) + assert [e.name for e in events[1:3]] == ["translated:sdk", "finalized"] + assert isinstance(events[3], MessagesSnapshotEvent) + assert isinstance(events[4], RunFinishedEvent) # ── AGUITranslator.to_agui (MESSAGES_SNAPSHOT) ─────────────────────────── @@ -155,10 +174,11 @@ def test_to_agui_emits_snapshot_by_default(): run_input = _run_input() async def collect(): - return [e async for e in translator.to_agui(result, run_input=run_input)] + return [e async for e in translator.to_agui(result, run_input)] events = asyncio.run(collect()) - snapshot = events[-1] + assert isinstance(events[-1], RunFinishedEvent) + snapshot = events[-2] assert isinstance(snapshot, MessagesSnapshotEvent) assert [m.id for m in snapshot.messages] == ["m1"] @@ -173,7 +193,7 @@ async def collect(): return [ e async for e in translator.to_agui( - result, run_input=_run_input(), emit_messages_snapshot=False + result, _run_input(), emit_messages_snapshot=False ) ] @@ -181,30 +201,76 @@ async def collect(): assert not any(isinstance(e, MessagesSnapshotEvent) for e in events) -def test_to_agui_skips_snapshot_without_run_input(): +def test_to_agui_emits_snapshot_for_bare_iterator_too(): + # The snapshot is built from the engine's own accumulator, not + # result.new_items, so a bare stream_events() iterator works the same + # as passing the RunResultStreaming object itself. translator = AGUITranslator(outbound_cls=_StubOutbound) - result = MagicMock(spec=RunResultStreaming) - result.stream_events.return_value = _fake_stream() - result.new_items = [] async def collect(): - return [e async for e in translator.to_agui(result)] + return [e async for e in translator.to_agui(_fake_stream("a"), _run_input())] events = asyncio.run(collect()) - assert not any(isinstance(e, MessagesSnapshotEvent) for e in events) + assert isinstance(events[-1], RunFinishedEvent) + assert isinstance(events[-2], MessagesSnapshotEvent) -def test_to_agui_emits_snapshot_for_bare_iterator_too(): - # The snapshot is built from the engine's own accumulator, not - # result.new_items, so a bare stream_events() iterator works the same - # as passing the RunResultStreaming object itself. +# ── AGUITranslator.to_agui (lifecycle events) ──────────────────────────── + + +def test_to_agui_wraps_stream_with_lifecycle_events(): translator = AGUITranslator(outbound_cls=_StubOutbound) async def collect(): - return [ - e - async for e in translator.to_agui(_fake_stream("a"), run_input=_run_input()) - ] + return [e async for e in translator.to_agui(_fake_stream("a"), _run_input())] events = asyncio.run(collect()) - assert isinstance(events[-1], MessagesSnapshotEvent) + started, finished = events[0], events[-1] + assert isinstance(started, RunStartedEvent) + assert started.thread_id == "t1" + assert started.run_id == "r1" + assert isinstance(finished, RunFinishedEvent) + assert finished.thread_id == "t1" + assert finished.run_id == "r1" + + +def test_to_agui_emits_run_error_and_reraises_on_exception(): + class _ExplodingOutbound(_StubOutbound): + def translate(self, sdk_event): + raise RuntimeError("boom") + + translator = AGUITranslator(outbound_cls=_ExplodingOutbound) + collected: list = [] + + async def collect(): + async for e in translator.to_agui(_fake_stream("a"), _run_input()): + collected.append(e) + + with pytest.raises(RuntimeError, match="boom"): + asyncio.run(collect()) + + assert isinstance(collected[0], RunStartedEvent) + assert isinstance(collected[-1], RunErrorEvent) + assert collected[-1].message == "boom" + + +def test_to_agui_emits_run_error_on_cancelled_error(): + # asyncio.CancelledError is BaseException, not Exception (3.8+) — a + # mid-stream timeout/dropped-connection must still surface RUN_ERROR + # instead of silently ending the stream after whatever was last yielded. + class _CancelledOutbound(_StubOutbound): + def translate(self, sdk_event): + raise asyncio.CancelledError() + + translator = AGUITranslator(outbound_cls=_CancelledOutbound) + collected: list = [] + + async def collect(): + async for e in translator.to_agui(_fake_stream("a"), _run_input()): + collected.append(e) + + with pytest.raises(asyncio.CancelledError): + asyncio.run(collect()) + + assert isinstance(collected[0], RunStartedEvent) + assert isinstance(collected[-1], RunErrorEvent) From 5280b107b3587aa2bf7fdc3723c0d4d2a14520ab Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Fri, 10 Jul 2026 01:03:10 +0200 Subject: [PATCH 11/94] feat(translator): add emit_run_error and run_error_message options to to_agui --- .../src/ag_ui_openai_agents/translator.py | 28 ++++++++++-- .../python/tests/test_translators.py | 43 +++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 3f4e44e13f..ecae89a7cf 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -76,6 +76,8 @@ async def to_agui( run_input: RunAgentInput, *, emit_messages_snapshot: bool = True, + emit_run_error: bool = True, + run_error_message: str | None = None, ) -> AsyncIterator[BaseEvent]: """Translate an SDK event stream into a live AG-UI event stream. @@ -88,8 +90,18 @@ async def to_agui( always RUN_FINISHED (or RUN_ERROR, if the stream raises — the error event is yielded and then the exception re-raised; this includes asyncio.CancelledError from a mid-stream timeout or - dropped connection, not just ordinary exceptions). Not optional. - thread_id/run_id come straight off run_input. + dropped connection, not just ordinary exceptions). RUN_STARTED / + RUN_FINISHED are not optional; thread_id/run_id come straight off + run_input. + + On error, the RUN_ERROR event carries str(exc) by default — pass + run_error_message to send a fixed string instead (e.g. a generic + "Agent run failed" so raw exception text never reaches the client). + The exception is re-raised regardless, so the caller's own logging + still sees the real one. Pass emit_run_error=False only if you + signal the terminal error yourself in an outer handler — otherwise + a raise with no RUN_ERROR leaves the client watching the stream + just stop. Just before RUN_FINISHED, a MESSAGES_SNAPSHOT is appended by default: run_input.messages, untouched, plus this run's messages — @@ -123,6 +135,12 @@ async def to_agui( half. emit_messages_snapshot: Whether to append a MESSAGES_SNAPSHOT just before RUN_FINISHED. Defaults to True. + emit_run_error: Whether to yield a RUN_ERROR event when the + stream raises, before re-raising. Defaults to True; set + False only if you emit your own terminal error event. + run_error_message: The RUN_ERROR message. Defaults to None, which + sends str(exc); set a fixed string to keep raw exception + text off the wire. Yields: AG-UI BaseEvent instances, ready to encode. @@ -152,7 +170,11 @@ async def to_agui( # a mid-stream cancellation (timeout, dropped connection) would # otherwise skip this handler entirely and the client would see # the stream just stop, with no RUN_ERROR and no RUN_FINISHED. - yield RunErrorEvent(type=EventType.RUN_ERROR, message=str(exc)) + if emit_run_error: + yield RunErrorEvent( + type=EventType.RUN_ERROR, + message=run_error_message or str(exc), + ) raise yield RunFinishedEvent( diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index 9034c2ff19..8eb5e61ce1 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -254,6 +254,49 @@ async def collect(): assert collected[-1].message == "boom" +def test_to_agui_emit_run_error_false_suppresses_event_but_still_raises(): + class _ExplodingOutbound(_StubOutbound): + def translate(self, sdk_event): + raise RuntimeError("boom") + + translator = AGUITranslator(outbound_cls=_ExplodingOutbound) + collected: list = [] + + async def collect(): + async for e in translator.to_agui( + _fake_stream("a"), _run_input(), emit_run_error=False + ): + collected.append(e) + + with pytest.raises(RuntimeError, match="boom"): + asyncio.run(collect()) + + assert isinstance(collected[0], RunStartedEvent) + assert not any(isinstance(e, RunErrorEvent) for e in collected) + + +def test_to_agui_run_error_message_overrides_exception_text(): + class _ExplodingOutbound(_StubOutbound): + def translate(self, sdk_event): + raise RuntimeError("raw internal detail") + + translator = AGUITranslator(outbound_cls=_ExplodingOutbound) + collected: list = [] + + async def collect(): + async for e in translator.to_agui( + _fake_stream("a"), _run_input(), run_error_message="Agent run failed" + ): + collected.append(e) + + with pytest.raises(RuntimeError, match="raw internal detail"): + asyncio.run(collect()) + + error = collected[-1] + assert isinstance(error, RunErrorEvent) + assert error.message == "Agent run failed" + + def test_to_agui_emits_run_error_on_cancelled_error(): # asyncio.CancelledError is BaseException, not Exception (3.8+) — a # mid-stream timeout/dropped-connection must still surface RUN_ERROR From 5c9ceb90dc4a404eb67ff48120d63c8be2f406b7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Fri, 10 Jul 2026 01:10:48 +0200 Subject: [PATCH 12/94] docs: document run_error_message, emit_run_error, and guardrail handling --- integrations/openai-agents/python/README.md | 41 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 9de99f7a5e..291f056df2 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -200,8 +200,9 @@ own logging/observability still sees it. This covers `asyncio.CancelledError` too (a mid-stream timeout or dropped connection), not just ordinary exceptions — it's `BaseException`, not `Exception`, so a plain `except Exception` would miss it and the client would just see the -stream stop with no `RUN_ERROR` and no `RUN_FINISHED`. Not optional — -every caller needs these three events, so there's no flag to turn them off: +stream stop with no `RUN_ERROR` and no `RUN_FINISHED`. `RUN_STARTED` and +`RUN_FINISHED` are not optional — every caller needs them, so there's no +flag to turn them off: ```python async for event in translator.to_agui(result, run_input): @@ -212,6 +213,22 @@ async for event in translator.to_agui(result, run_input): pass, since `run_input` is already required for the lifecycle events (and, by default, the snapshot). +The `RUN_ERROR` on the error path is tunable. By default it carries +`str(exc)`; pass `run_error_message` to send a fixed string instead, so raw +exception text never reaches the client — the real exception is still +re-raised, so your own logging keeps it: + +```python +async for event in translator.to_agui( + result, run_input, run_error_message="Agent run failed" +): + yield encoder.encode(event) +``` + +Pass `emit_run_error=False` only if you emit your own terminal error event +in an outer handler — otherwise the exception re-raises with no `RUN_ERROR` +and the client just watches the stream stop. + ### Messages Snapshot `MESSAGES_SNAPSHOT` lets the client resync its whole message list in one @@ -271,6 +288,26 @@ The translator translates. Your run loop still owns orchestration: This keeps the integration framework-neutral. FastAPI, Starlette, Django, aiohttp, raw ASGI, WebSockets, or tests can all use the same translator calls. +### Guardrails + +The AG-UI protocol has no guardrail event type, so guardrails surface as +run errors. When an input or output guardrail trips, the OpenAI Agents SDK +raises `InputGuardrailTripwireTriggered` / `OutputGuardrailTripwireTriggered` +mid-run; that exception flows through the stream, `to_agui` yields +`RUN_ERROR`, and re-raises. No special handling needed — a tripwire aborts +the run like any other error. + +Guardrail messages can be noisy or leak your policy internals, so this is a +natural place for `run_error_message` — send a clean, client-safe string +while your logs keep the real exception: + +```python +async for event in translator.to_agui( + result, run_input, run_error_message="Request blocked by content policy" +): + yield encoder.encode(event) +``` + ### Transport Options For SSE, use the AG-UI SDK's own encoder — it produces one `data:` frame per From aed91dfe272fbfef1d6833d2ce9736d876b02668 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Fri, 10 Jul 2026 12:37:49 +0200 Subject: [PATCH 13/94] feat(translator): echo run_input.state as STATE_SNAPSHOT Add emit_state_snapshot (default True): after RUN_STARTED, echo run_input.state as a STATE_SNAPSHOT including empty state --- .../src/ag_ui_openai_agents/translator.py | 18 +++++ .../python/tests/test_translators.py | 77 ++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index ecae89a7cf..cdc4e7c52c 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -23,6 +23,7 @@ RunErrorEvent, RunFinishedEvent, RunStartedEvent, + StateSnapshotEvent, ) from .engine.agui_to_sdk import AGUIToSDKTranslator @@ -78,6 +79,7 @@ async def to_agui( emit_messages_snapshot: bool = True, emit_run_error: bool = True, run_error_message: str | None = None, + emit_state_snapshot: bool = True, ) -> AsyncIterator[BaseEvent]: """Translate an SDK event stream into a live AG-UI event stream. @@ -113,6 +115,13 @@ async def to_agui( object or a bare stream_events() iterator — the snapshot is built from engine state, not result.new_items. + Right after RUN_STARTED, a STATE_SNAPSHOT echoes run_input.state + back whenever it isn't None — the same thing the other AG-UI + integrations that have no native SDK state do (the OpenAI Agents + SDK has no shared-state channel of its own; state lives at the + AG-UI/frontend layer). Pass emit_state_snapshot=False to suppress + it. Nothing state-related is emitted on the error path. + Note: run_input.messages passes through to the snapshot as-is. If it carries messages your client should not see echoed back — a @@ -141,6 +150,9 @@ async def to_agui( run_error_message: The RUN_ERROR message. Defaults to None, which sends str(exc); set a fixed string to keep raw exception text off the wire. + emit_state_snapshot: Whether to echo run_input.state as a + STATE_SNAPSHOT after RUN_STARTED when it isn't None. + Defaults to True. Yields: AG-UI BaseEvent instances, ready to encode. @@ -151,6 +163,12 @@ async def to_agui( run_id=run_input.run_id, ) + if emit_state_snapshot and run_input.state is not None: + yield StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=run_input.state, + ) + stream_events = ( events.stream_events() if isinstance(events, RunResultStreaming) diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index 8eb5e61ce1..258b437c94 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -29,6 +29,7 @@ RunErrorEvent, RunFinishedEvent, RunStartedEvent, + StateSnapshotEvent, Tool, UserMessage, ) @@ -118,7 +119,10 @@ async def collect(): return [ e async for e in translator.to_agui( - _fake_stream("a", "b"), _run_input(), emit_messages_snapshot=False + _fake_stream("a", "b"), + _run_input(), + emit_messages_snapshot=False, + emit_state_snapshot=False, ) ] @@ -154,7 +158,12 @@ def test_streaming_to_agui_accepts_run_streaming_result(): result.new_items = [] async def collect(): - return [e async for e in translator.to_agui(result, _run_input())] + return [ + e + async for e in translator.to_agui( + result, _run_input(), emit_state_snapshot=False + ) + ] events = asyncio.run(collect()) assert isinstance(events[0], RunStartedEvent) @@ -317,3 +326,67 @@ async def collect(): assert isinstance(collected[0], RunStartedEvent) assert isinstance(collected[-1], RunErrorEvent) + + +# ── AGUITranslator.to_agui (state snapshot) ────────────────────────────── + + +def test_to_agui_echoes_run_input_state_as_snapshot(): + # Non-empty run_input.state is echoed back as a STATE_SNAPSHOT right + # after RUN_STARTED — matching the other integrations with no native SDK + # state. + translator = AGUITranslator(outbound_cls=_StubOutbound) + run_input = _run_input().model_copy(update={"state": {"theme": "dark"}}) + + async def collect(): + return [ + e + async for e in translator.to_agui( + _fake_stream("a"), run_input, emit_messages_snapshot=False + ) + ] + + events = asyncio.run(collect()) + assert isinstance(events[0], RunStartedEvent) + assert isinstance(events[1], StateSnapshotEvent) + assert events[1].snapshot == {"theme": "dark"} + # Just the one snapshot — no mid-run state tracking. + assert sum(isinstance(e, StateSnapshotEvent) for e in events) == 1 + + +def test_to_agui_empty_run_input_state_still_emits_snapshot(): + # Gated on `is not None`, so an empty {} still echoes as STATE_SNAPSHOT({}) + # — matching aws-strands / claude-agent-sdk. + translator = AGUITranslator(outbound_cls=_StubOutbound) + + async def collect(): + return [ + e + async for e in translator.to_agui( + _fake_stream("a"), _run_input(), emit_messages_snapshot=False + ) + ] + + events = asyncio.run(collect()) + snapshots = [e for e in events if isinstance(e, StateSnapshotEvent)] + assert len(snapshots) == 1 + assert snapshots[0].snapshot == {} + + +def test_to_agui_emit_state_snapshot_false_suppresses_it(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + run_input = _run_input().model_copy(update={"state": {"x": 1}}) + + async def collect(): + return [ + e + async for e in translator.to_agui( + _fake_stream("a"), + run_input, + emit_messages_snapshot=False, + emit_state_snapshot=False, + ) + ] + + events = asyncio.run(collect()) + assert not any(isinstance(e, StateSnapshotEvent) for e in events) From f3fd39cf415d8438e293e7fbef0a909e0db9c1fb Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 11 Jul 2026 01:21:25 +0200 Subject: [PATCH 14/94] docs: expand human-in-the-loop section in README --- integrations/openai-agents/python/README.md | 44 ++++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 291f056df2..389752836b 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -415,25 +415,49 @@ class MyOutbound(SDKToAGUITranslator): translator = AGUITranslator(outbound_cls=MyOutbound) ``` -## Frontend (client-owned) tools +## Frontend (client-owned) tools & Human-in-the-Loop -Tools declared in `RunAgentInput.tools` belong to the frontend. `to_sdk` -turns them into SDK `FunctionTool` proxies so the model can call them; for -human-in-the-loop flows, end the run the moment such a tool is called by -using the SDK's native mechanism: +Tools declared in `RunAgentInput.tools` belong to the **frontend** — the +browser owns their execution (rendering UI, waiting on the user), not your +server. This is the human-in-the-loop mechanism: the same one most AG-UI +integrations use, paired with CopilotKit's `useHumanInTheLoop` on the client. + +**1. Merge the client tools onto your agent per request.** `to_sdk` turns +each into an SDK `FunctionTool` proxy: + +```python +translated = translator.to_sdk(run_input) +agent = base_agent.clone(tools=[*base_agent.tools, *translated.tools]) +``` + +**2. Stop the run when the model calls one.** Use the SDK-native +`StopAtTools` so the run ends the instant the model emits the call — before +the (never-used) proxy body would run: ```python from agents import Agent, StopAtTools -agent = Agent( +base_agent = Agent( ..., - tool_use_behavior=StopAtTools(stop_at_tool_names=["confirm_changes"]), + tool_use_behavior=StopAtTools(stop_at_tool_names=["generate_task_steps"]), ) ``` -The run stops before the proxy body executes; the frontend answers the tool -call and sends the result back as a `tool` message in the next run's -`messages`. See `examples/agents_examples/human_in_the_loop.py`. +**3. The frontend answers; the agent resumes next request.** The tool call +streams to the browser, the user acts, and the result comes back as a +`ToolMessage` in the next run's `messages` — ordinary multi-turn history. +`to_sdk` translates that `ToolMessage` into a `function_call_output`, so the +agent picks up where it left off. No custom event, no `RunState`, no +persistence to wire. + +``` +Request 1: user asks → agent calls generate_task_steps → RUN_FINISHED (paused) +Frontend: renders the call, user approves/edits +Request 2: messages include the ToolMessage result → agent continues +``` + +See `examples/agents_examples/human_in_the_loop.py` for the agent and +`examples/server.py` for the run loop that merges the client tools. ## Gotchas From 7616b86d0453ba1826ceb549791774441d8337ad Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 11 Jul 2026 02:27:17 +0200 Subject: [PATCH 15/94] feat(openai-agents): add OpenAIAgentsAgent wrapper and FastAPI endpoint helper --- integrations/openai-agents/python/README.md | 66 ++++++++++++- .../openai-agents/python/pyproject.toml | 1 + .../src/ag_ui_openai_agents/__init__.py | 18 +++- .../python/src/ag_ui_openai_agents/agent.py | 94 +++++++++++++++++++ .../src/ag_ui_openai_agents/endpoint.py | 50 ++++++++++ integrations/openai-agents/python/uv.lock | 73 +++++++++++++- 6 files changed, 294 insertions(+), 8 deletions(-) create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py create mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 389752836b..e19d47fa12 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -44,11 +44,55 @@ For local development this package uses [uv](https://docs.astral.sh/uv/): uv sync ``` -## Quick Start: Streaming Endpoint +## Quick Start: Serve an Agent -This is the common server shape for AG-UI clients: accept `RunAgentInput`, run -your OpenAI agent with `Runner.run_streamed`, and stream AG-UI events back over -SSE. +The fastest path. Wrap a plain SDK `Agent` with `OpenAIAgentsAgent`, then hand +it to `add_openai_agents_fastapi_endpoint` — SSE, content negotiation, a +`/health` check, lifecycle events, and the state/messages snapshots are all +wired for you. + +```python +from agents import Agent +from fastapi import FastAPI + +from ag_ui_openai_agents import ( + OpenAIAgentsAgent, + add_openai_agents_fastapi_endpoint, +) + +agent = OpenAIAgentsAgent(Agent(name="assistant", instructions="Be concise.")) + +app = FastAPI() +add_openai_agents_fastapi_endpoint(app, agent, "/") +``` + +That's the whole integration. `OpenAIAgentsAgent`: + +- holds no per-request state — one instance serves every request; the SDK + `Agent` is a config template, and client-declared tools are merged onto a + per-request `clone()`, so concurrent requests never see each other's tools; +- takes `run_config=...` to route runs through a non-OpenAI provider (e.g. + LiteLLM) or set run-wide model settings; +- exposes `run(RunAgentInput) -> AsyncIterator[BaseEvent]` if you want to serve + it on a transport other than FastAPI. + +```python +from agents import RunConfig +from agents.extensions.models.litellm_provider import LitellmProvider + +agent = OpenAIAgentsAgent( + Agent(name="assistant", instructions="Be concise.", model="gemini/gemini-2.5-flash"), + run_config=RunConfig(model_provider=LitellmProvider()), +) +``` + +Need finer control (a custom transport, your own SSE framing, per-run branching)? +Drop to the translator — see the next section. + +## Quick Start: Compose It Yourself + +The lower-level path the wrapper is built on: accept `RunAgentInput`, run your +OpenAI agent with `Runner.run_streamed`, and stream AG-UI events back over SSE. ```python from agents import Agent, Runner @@ -107,7 +151,18 @@ orchestrator) lives in [`examples/`](examples/). ## Public API -There is one public translator; it pairs with the SDK's streaming run mode: +Two layers, pick by how much control you want: + +| Name | Kind | Use it for | +|---|---|---| +| `OpenAIAgentsAgent` | wrapper class | serve an agent: `run(RunAgentInput) -> AsyncIterator[BaseEvent]` | +| `add_openai_agents_fastapi_endpoint(app, agent, path)` | helper | wire a wrapped agent to FastAPI (SSE + `/health`) | +| `AGUITranslator` | translator | compose it yourself — `to_sdk` + `to_agui` | + +The wrapper is built on the translator; use the translator directly for custom +transports or per-run branching. + +`AGUITranslator` pairs with the SDK's streaming run mode: | Class | Pairs with | `to_agui(...)` returns | |---|---|---| @@ -120,6 +175,7 @@ fresh per-run engine it needs. Create one instance and share it. | Need | Use | |---|---| +| Just serve an agent over FastAPI | `OpenAIAgentsAgent` + `add_openai_agents_fastapi_endpoint` | | Live chat, tool-call progress, reasoning progress | `AGUITranslator` with `Runner.run_streamed` | | FastAPI SSE | Return `StreamingResponse(_stream(...), media_type="text/event-stream")` | | WebSocket or another async transport | Iterate `translator.to_agui(result, run_input)` and send each event JSON | diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml index 3cdd02d2d7..d251f21ab1 100644 --- a/integrations/openai-agents/python/pyproject.toml +++ b/integrations/openai-agents/python/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "ag-ui-protocol>=0.1.18", "openai-agents>=0.8.4", "pydantic>=2.13.4", + "fastapi>=0.115.12", ] [dependency-groups] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py index 13d0c48cd5..82b5d4dfb9 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py @@ -1,6 +1,17 @@ """OpenAI Agents SDK × AG-UI Protocol integration. -Primary API — the streaming translator: +Serve an agent (highest level — wrapper + FastAPI helper): + + from agents import Agent + from ag_ui_openai_agents import ( + OpenAIAgentsAgent, + add_openai_agents_fastapi_endpoint, + ) + + agent = OpenAIAgentsAgent(Agent(name="assistant", instructions="...")) + add_openai_agents_fastapi_endpoint(app, agent, "/") + +Compose it yourself (mid level — the streaming translator): from ag_ui_openai_agents import AGUITranslator @@ -11,6 +22,8 @@ from __future__ import annotations +from .agent import OpenAIAgentsAgent +from .endpoint import add_openai_agents_fastapi_endpoint from .engine import ( AGUIToSDKTranslator, ClientToolPending, @@ -22,6 +35,9 @@ __version__ = "0.1.0" __all__ = [ + # Serve an agent (highest level) + "OpenAIAgentsAgent", + "add_openai_agents_fastapi_endpoint", # Public translator (primary API — 2 methods) "AGUITranslator", # Engine translators (advanced / per-mapping overrides) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py new file mode 100644 index 0000000000..a6c9b1e3b3 --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -0,0 +1,94 @@ +"""Agent wrapper — an OpenAI Agents SDK Agent that speaks AG-UI. + +Wrap a plain SDK Agent, get one run(RunAgentInput) yielding AG-UI events, +ready for add_openai_agents_fastapi_endpoint. +""" + +from __future__ import annotations + +from typing import AsyncIterator + +from agents import Agent, RunConfig, Runner +from ag_ui.core import BaseEvent, RunAgentInput + +from .translator import AGUITranslator + + +class OpenAIAgentsAgent: + """Wrap an OpenAI Agents SDK Agent so it speaks the AG-UI protocol. + + One instance per server process is fine. The wrapper holds no per-request + state: an SDK Agent is a config template that Runner.run_streamed never + mutates, and AGUITranslator is stateless on the inbound side and spins up a + fresh outbound engine per to_agui call. Client-declared tools arriving on a + request are merged onto a per-request agent.clone(), so concurrent requests + never see each other's tools. + + Example: + from agents import Agent + from ag_ui_openai_agents import OpenAIAgentsAgent + + sdk_agent = Agent(name="assistant", instructions="You are helpful.") + agent = OpenAIAgentsAgent(sdk_agent) + + async for event in agent.run(run_input): + ... + """ + + def __init__( + self, + agent: Agent, + *, + name: str | None = None, + description: str = "", + translator: AGUITranslator | None = None, + run_config: RunConfig | None = None, + ) -> None: + """Wrap an SDK Agent. + + Args: + agent: The OpenAI Agents SDK Agent to serve. + name: Public name for health/introspection. Defaults to agent.name. + description: Optional human-readable description. + translator: An AGUITranslator to reuse. Defaults to a fresh one; + pass your own to inject engine subclasses (per-mapping overrides). + run_config: Passed straight to Runner.run_streamed on every run — + the place to set a non-OpenAI model provider (e.g. LiteLLM) or + run-wide model settings. None uses the SDK defaults (native OpenAI). + """ + self.agent = agent + self.name = name or agent.name + self.description = description + self._translator = translator or AGUITranslator() + self._run_config = run_config + + async def run(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: + """Run the agent for one AG-UI request and yield AG-UI events. + + Orchestration only — the translator does the mapping. to_sdk turns the + request into SDK input items plus FunctionTool proxies for any + client-declared tools; those proxies are merged onto a per-request + clone so the static agent stays untouched. to_agui wraps the SDK stream + with the lifecycle events (RUN_STARTED/FINISHED/ERROR), the STATE and + MESSAGES snapshots, and the flush — nothing to hand-roll here. + + Args: + input: The incoming AG-UI RunAgentInput. + + Yields: + AG-UI BaseEvent instances, ready to encode. + """ + translated = self._translator.to_sdk(input) + + agent = self.agent + if translated.tools: + agent = agent.clone(tools=[*self.agent.tools, *translated.tools]) + + result = Runner.run_streamed( + agent, + input=translated.messages, + run_config=self._run_config, + ) + + async for event in self._translator.to_agui(result, input): + yield event diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py new file mode 100644 index 0000000000..5609739f76 --- /dev/null +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py @@ -0,0 +1,50 @@ +"""FastAPI endpoint helper for the OpenAI Agents SDK integration. + +Glue an OpenAIAgentsAgent to a FastAPI app in one call: SSE stream, content +negotiation, health check. +""" + +from __future__ import annotations + +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse + +from ag_ui.core import RunAgentInput +from ag_ui.encoder import EventEncoder + +from .agent import OpenAIAgentsAgent + + +def add_openai_agents_fastapi_endpoint( + app: FastAPI, + agent: OpenAIAgentsAgent, + path: str = "/", +) -> None: + """Add an OpenAI Agents SDK agent endpoint to a FastAPI app. + + Args: + app: The FastAPI application to register the route on. + agent: The wrapped agent to serve. + path: The POST path for the agent. Defaults to "/". A GET health check + is registered alongside it. + """ + + @app.post(path) + async def openai_agents_endpoint(input_data: RunAgentInput, request: Request): + """Run the agent and stream AG-UI events back to the client.""" + accept_header = request.headers.get("accept") + encoder = EventEncoder(accept=accept_header) + + async def event_generator(): + async for event in agent.run(input_data): + yield encoder.encode(event) + + return StreamingResponse( + event_generator(), + media_type=encoder.get_content_type(), + ) + + @app.get(path.rstrip("/") + "/health") + def health(): + """Health check.""" + return {"status": "ok", "agent": {"name": agent.name}} diff --git a/integrations/openai-agents/python/uv.lock b/integrations/openai-agents/python/uv.lock index 3bc1e7e05f..bdb5d8b17b 100644 --- a/integrations/openai-agents/python/uv.lock +++ b/integrations/openai-agents/python/uv.lock @@ -12,6 +12,8 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "ag-ui-protocol" }, + { name = "fastapi", version = "0.128.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fastapi", version = "0.139.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "openai-agents", version = "0.8.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "openai-agents", version = "0.17.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pydantic" }, @@ -26,6 +28,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "ag-ui-protocol", specifier = ">=0.1.18" }, + { name = "fastapi", specifier = ">=0.115.12" }, { name = "openai-agents", specifier = ">=0.8.4" }, { name = "pydantic", specifier = ">=2.13.4" }, ] @@ -45,6 +48,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/74/913c9b8fc566c6da650aecbddf25a5d8186b54138df265eb9eb546f56141/ag_ui_protocol-0.1.18-py3-none-any.whl", hash = "sha256:d151c0f0a34160647f1571163f7185746f4326b15a56d1560de5082a7a0e7a12", size = 12607, upload-time = "2026-04-21T20:45:00.097Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -423,6 +435,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "fastapi" +version = "0.128.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, +] + +[[package]] +name = "fastapi" +version = "0.139.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" }, +] + [[package]] name = "griffe" version = "1.14.0" @@ -682,7 +732,7 @@ dependencies = [ { name = "python-multipart", marker = "python_full_version >= '3.10'" }, { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, - { name = "starlette", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, { name = "uvicorn", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, @@ -1238,17 +1288,36 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "starlette", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, ] +[[package]] +name = "starlette" +version = "0.49.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, +] + [[package]] name = "starlette" version = "1.0.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] dependencies = [ { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, From 5e306d079e0535ca8634add5bf63e7c7d2a44204 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 11 Jul 2026 02:35:52 +0200 Subject: [PATCH 16/94] feat(openai-agents): add tests for OpenAIAgentsAgent wrapper and FastAPI endpoint --- .../openai-agents/python/tests/test_agent.py | 121 ++++++++++++++++++ .../python/tests/test_endpoint.py | 86 +++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 integrations/openai-agents/python/tests/test_agent.py create mode 100644 integrations/openai-agents/python/tests/test_endpoint.py diff --git a/integrations/openai-agents/python/tests/test_agent.py b/integrations/openai-agents/python/tests/test_agent.py new file mode 100644 index 0000000000..a3588d5f56 --- /dev/null +++ b/integrations/openai-agents/python/tests/test_agent.py @@ -0,0 +1,121 @@ +""" +Tests for the OpenAIAgentsAgent wrapper. + +Covers orchestration only — the event mapping itself is the translator's job +and has its own coverage: + +- ``run`` yields the translator-wrapped stream (RUN_STARTED first, + RUN_FINISHED last) for one AG-UI request. +- Client-declared tools are merged onto a per-request ``clone`` — the wrapped + static agent is never mutated; without tools no clone happens. +- ``run_config`` passes through to ``Runner.run_streamed``. +- ``name`` defaults to the SDK agent's name. +""" + +from __future__ import annotations + +import asyncio +from unittest.mock import MagicMock + +import pytest +from ag_ui.core import ( + EventType, + RunAgentInput, + Tool, + UserMessage, +) +from agents import Agent, RunConfig +from agents.result import RunResultStreaming + +from ag_ui_openai_agents import OpenAIAgentsAgent +import ag_ui_openai_agents.agent as agent_module + + +def _run_input(with_tool: bool = False) -> RunAgentInput: + return RunAgentInput( + thread_id="t1", + run_id="r1", + messages=[UserMessage(id="m1", role="user", content="hi")], + tools=[ + Tool( + name="confirm", + description="Ask the user to confirm.", + parameters={"type": "object", "properties": {}}, + ) + ] + if with_tool + else [], + state={}, + context=[], + forwarded_props=None, + ) + + +async def _empty_stream(): + return + yield # pragma: no cover — makes this an async generator + + +@pytest.fixture +def patched_runner(monkeypatch): + """Replace Runner.run_streamed with a capture that returns an empty stream.""" + calls: list[dict] = [] + + def fake_run_streamed(agent, *, input, run_config=None, **kwargs): + calls.append({"agent": agent, "input": input, "run_config": run_config}) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _empty_stream() + return result + + monkeypatch.setattr(agent_module.Runner, "run_streamed", fake_run_streamed) + return calls + + +def _collect(wrapper: OpenAIAgentsAgent, run_input: RunAgentInput) -> list: + async def go(): + return [event async for event in wrapper.run(run_input)] + + return asyncio.run(go()) + + +def test_run_wraps_stream_with_lifecycle(patched_runner): + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi")) + events = _collect(wrapper, _run_input()) + assert events[0].type == EventType.RUN_STARTED + assert events[-1].type == EventType.RUN_FINISHED + assert events[0].thread_id == "t1" + assert events[0].run_id == "r1" + + +def test_run_without_client_tools_uses_static_agent(patched_runner): + sdk_agent = Agent(name="assistant", instructions="hi") + wrapper = OpenAIAgentsAgent(sdk_agent) + _collect(wrapper, _run_input()) + assert patched_runner[0]["agent"] is sdk_agent + + +def test_run_merges_client_tools_onto_clone(patched_runner): + sdk_agent = Agent(name="assistant", instructions="hi") + wrapper = OpenAIAgentsAgent(sdk_agent) + _collect(wrapper, _run_input(with_tool=True)) + ran_agent = patched_runner[0]["agent"] + assert ran_agent is not sdk_agent, "client tools must go on a clone" + assert [t.name for t in ran_agent.tools] == ["confirm"] + assert sdk_agent.tools == [], "the static agent must stay untouched" + + +def test_run_config_passes_through(patched_runner): + run_config = RunConfig() + wrapper = OpenAIAgentsAgent( + Agent(name="assistant", instructions="hi"), run_config=run_config + ) + _collect(wrapper, _run_input()) + assert patched_runner[0]["run_config"] is run_config + + +def test_name_defaults_to_agent_name(): + assert OpenAIAgentsAgent(Agent(name="helper", instructions="hi")).name == "helper" + assert ( + OpenAIAgentsAgent(Agent(name="helper", instructions="hi"), name="api").name + == "api" + ) diff --git a/integrations/openai-agents/python/tests/test_endpoint.py b/integrations/openai-agents/python/tests/test_endpoint.py new file mode 100644 index 0000000000..bfadf2d41e --- /dev/null +++ b/integrations/openai-agents/python/tests/test_endpoint.py @@ -0,0 +1,86 @@ +""" +Tests for add_openai_agents_fastapi_endpoint. + +Wiring only — the wrapper's behavior is covered in test_agent.py: + +- POST on the given path streams the run as SSE frames. +- A GET health check is registered next to it and reports the agent name. +- Custom (non-root) paths get their health check at /health. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest +from agents import Agent +from agents.result import RunResultStreaming +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint +import ag_ui_openai_agents.agent as agent_module + +RUN_INPUT_JSON = { + "thread_id": "t1", + "run_id": "r1", + "messages": [{"id": "m1", "role": "user", "content": "hi"}], + "tools": [], + "state": {}, + "context": [], + "forwarded_props": None, +} + + +async def _empty_stream(): + return + yield # pragma: no cover — makes this an async generator + + +@pytest.fixture +def client(monkeypatch) -> TestClient: + def fake_run_streamed(agent, *, input, run_config=None, **kwargs): + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _empty_stream() + return result + + monkeypatch.setattr(agent_module.Runner, "run_streamed", fake_run_streamed) + + app = FastAPI() + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi")) + add_openai_agents_fastapi_endpoint(app, wrapper, "/") + return TestClient(app) + + +def test_post_streams_sse_run(client): + with client.stream("POST", "/", json=RUN_INPUT_JSON) as response: + assert response.status_code == 200 + assert response.headers["content-type"].startswith("text/event-stream") + body = "".join(response.iter_text()) + assert '"RUN_STARTED"' in body + assert '"RUN_FINISHED"' in body + assert body.count("data: ") >= 2, "each event must be its own SSE frame" + + +def test_health_reports_agent_name(client): + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "ok", "agent": {"name": "assistant"}} + + +def test_custom_path_places_health_under_it(monkeypatch): + def fake_run_streamed(agent, *, input, run_config=None, **kwargs): + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _empty_stream() + return result + + monkeypatch.setattr(agent_module.Runner, "run_streamed", fake_run_streamed) + + app = FastAPI() + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi")) + add_openai_agents_fastapi_endpoint(app, wrapper, "/my_agent") + client = TestClient(app) + + assert client.get("/my_agent/health").status_code == 200 + with client.stream("POST", "/my_agent", json=RUN_INPUT_JSON) as response: + assert response.status_code == 200 From 96f88e0647604ed24bb6d01f2b65f2923e6becc3 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 11 Jul 2026 02:49:19 +0200 Subject: [PATCH 17/94] test(openai-agents): add streaming event mapping coverage for SDKToAGUITranslator --- .../python/tests/test_engine_mapping.py | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 integrations/openai-agents/python/tests/test_engine_mapping.py diff --git a/integrations/openai-agents/python/tests/test_engine_mapping.py b/integrations/openai-agents/python/tests/test_engine_mapping.py new file mode 100644 index 0000000000..32ab17aa75 --- /dev/null +++ b/integrations/openai-agents/python/tests/test_engine_mapping.py @@ -0,0 +1,343 @@ +""" +Tests for SDKToAGUITranslator's streaming event mapping. + +The rest of the suite pins ids (test_snapshot) and the wire strings +(test_stream_types_drift); this one pins the actual translation: feed the +engine the SDK stream events a real run emits — raw Responses deltas, +run-item commits, agent-updated signals — and assert the exact AG-UI event +sequence it yields. No network, no model; just the mapping. + +Events are driven through ``translate`` (the tier-1 dispatcher ``to_agui`` +calls per event), so raw dispatch, family dispatch, and the per-type methods +are all on the path. Raw deltas are duck-typed SimpleNamespaces (the engine +reads them via ``read_attr``, exactly as it does the real payloads); run +items use the SDK's own item classes so the isinstance dispatch is real. +""" + +from __future__ import annotations + +from types import SimpleNamespace + +from agents import ( + Agent, + HandoffCallItem, + HandoffOutputItem, + MCPApprovalRequestItem, +) +from agents.items import ToolCallOutputItem +from ag_ui.core import EventType +from openai.types.responses import ResponseFunctionToolCall +from openai.types.responses.response_output_item import McpApprovalRequest + +from ag_ui_openai_agents.engine import SDKToAGUITranslator +from ag_ui_openai_agents.engine.stream_types import ( + RawResponseEventType, + SDKItemType, + SDKStreamEventType, +) + +_AGENT = Agent(name="test-agent") + + +# ── event builders — shaped like the SDK's real stream events ──────────── + + +def _raw(kind: RawResponseEventType, **data) -> SimpleNamespace: + """A RawResponsesStreamEvent wrapping one Responses payload.""" + return SimpleNamespace( + type=SDKStreamEventType.RAW_RESPONSE, + data=SimpleNamespace(type=kind, **data), + ) + + +def _added(item_type: SDKItemType, output_index: int = 0, **item) -> SimpleNamespace: + return _raw( + RawResponseEventType.OUTPUT_ITEM_ADDED, + item=SimpleNamespace(type=item_type, **item), + output_index=output_index, + ) + + +def _done(item_type: SDKItemType, output_index: int = 0, **item) -> SimpleNamespace: + return _raw( + RawResponseEventType.OUTPUT_ITEM_DONE, + item=SimpleNamespace(type=item_type, **item), + output_index=output_index, + ) + + +def _run_item(item) -> SimpleNamespace: + return SimpleNamespace(type=SDKStreamEventType.RUN_ITEM, name="x", item=item) + + +def _agent_updated(name: str) -> SimpleNamespace: + return SimpleNamespace( + type=SDKStreamEventType.AGENT_UPDATED, + new_agent=SimpleNamespace(name=name), + ) + + +def _drive(engine: SDKToAGUITranslator, *events) -> list: + out: list = [] + for event in events: + out.extend(engine.translate(event)) + return out + + +def _types(events) -> list[EventType]: + return [e.type for e in events] + + +# ── text streaming ─────────────────────────────────────────────────────── + + +def test_text_streams_start_content_end_under_one_id(): + engine = SDKToAGUITranslator() + events = _drive( + engine, + _added(SDKItemType.MESSAGE, id="msg_1"), + _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="Hel"), + _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="lo"), + _done(SDKItemType.MESSAGE, id="msg_1"), + ) + assert _types(events) == [ + EventType.TEXT_MESSAGE_START, + EventType.TEXT_MESSAGE_CONTENT, + EventType.TEXT_MESSAGE_CONTENT, + EventType.TEXT_MESSAGE_END, + ] + ids = {e.message_id for e in events} + assert ids == {"msg_1"}, "the whole window must carry the real item id" + assert events[0].role == "assistant" + assert [e.delta for e in events if e.type == EventType.TEXT_MESSAGE_CONTENT] == [ + "Hel", + "lo", + ] + + +def test_text_delta_lazily_opens_window_without_output_item_added(): + # Some backends jump straight to deltas with no output_item.added first. + engine = SDKToAGUITranslator() + events = _drive( + engine, + _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), + ) + assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] + + +def test_text_done_closes_window_when_output_item_done_is_skipped(): + engine = SDKToAGUITranslator() + events = _drive( + engine, + _added(SDKItemType.MESSAGE, id="msg_1"), + _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), + _raw(RawResponseEventType.TEXT_DONE, item_id="msg_1", output_index=0), + ) + assert _types(events)[-1] == EventType.TEXT_MESSAGE_END + + +def test_refusal_delta_streams_into_the_text_window(): + engine = SDKToAGUITranslator() + events = _drive( + engine, + _added(SDKItemType.MESSAGE, id="msg_1"), + _raw(RawResponseEventType.REFUSAL_DELTA, item_id="msg_1", output_index=0, delta="no"), + ) + assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] + assert events[1].delta == "no" + + +# ── tool-call streaming ────────────────────────────────────────────────── + + +def test_function_call_streams_start_args_end_under_the_call_id(): + engine = SDKToAGUITranslator() + events = _drive( + engine, + _added(SDKItemType.FUNCTION_CALL, id="fc_1", call_id="call_1", name="get_weather"), + _raw( + RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + item_id="fc_1", + output_index=0, + delta='{"city":', + ), + _raw( + RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + item_id="fc_1", + output_index=0, + delta='"Cairo"}', + ), + _done(SDKItemType.FUNCTION_CALL, id="fc_1", call_id="call_1", name="get_weather"), + ) + assert _types(events) == [ + EventType.TOOL_CALL_START, + EventType.TOOL_CALL_ARGS, + EventType.TOOL_CALL_ARGS, + EventType.TOOL_CALL_END, + ] + assert {e.tool_call_id for e in events} == {"call_1"} + assert events[0].tool_call_name == "get_weather" + assert "".join(e.delta for e in events if e.type == EventType.TOOL_CALL_ARGS) == ( + '{"city":"Cairo"}' + ) + + +def test_function_call_args_delta_lazily_opens_the_call(): + engine = SDKToAGUITranslator() + events = _drive( + engine, + _raw( + RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + item_id="fc_1", + output_index=0, + delta="{}", + ), + ) + assert _types(events) == [EventType.TOOL_CALL_START, EventType.TOOL_CALL_ARGS] + + +def test_tool_output_item_emits_tool_call_result(): + engine = SDKToAGUITranslator() + item = ToolCallOutputItem( + agent=_AGENT, + raw_item={"type": "function_call_output", "call_id": "call_1", "output": "sunny"}, + output="sunny", + ) + events = _drive(engine, _run_item(item)) + assert _types(events) == [EventType.TOOL_CALL_RESULT] + assert events[0].tool_call_id == "call_1" + assert events[0].content == "sunny" + + +# ── reasoning streaming ────────────────────────────────────────────────── + + +def test_reasoning_summary_delta_opens_phase_and_part(): + engine = SDKToAGUITranslator() + events = _drive( + engine, + _raw( + RawResponseEventType.REASONING_SUMMARY_DELTA, + item_id="rs_1", + output_index=0, + delta="thinking", + ), + _raw(RawResponseEventType.REASONING_SUMMARY_PART_DONE, item_id="rs_1", output_index=0), + ) + assert _types(events) == [ + EventType.REASONING_START, + EventType.REASONING_MESSAGE_START, + EventType.REASONING_MESSAGE_CONTENT, + EventType.REASONING_MESSAGE_END, + ] + assert events[2].delta == "thinking" + # The phase stays open until finalize (only the part closed above). + assert _types(engine.finalize()) == [EventType.REASONING_END] + + +def test_reasoning_auto_closes_when_text_output_starts(): + # Once real output begins, any open reasoning must be closed first — + # reasoning must never bleed into the answer window. + engine = SDKToAGUITranslator() + _drive( + engine, + _raw( + RawResponseEventType.REASONING_TEXT_DELTA, + item_id="rs_1", + output_index=0, + delta="hmm", + ), + ) + events = _drive(engine, _added(SDKItemType.MESSAGE, id="msg_1")) + assert _types(events) == [ + EventType.REASONING_MESSAGE_END, + EventType.REASONING_END, + EventType.TEXT_MESSAGE_START, + ] + + +# ── multi-agent steps ──────────────────────────────────────────────────── + + +def test_agent_updated_opens_a_step_finalize_closes_it(): + engine = SDKToAGUITranslator() + events = _drive(engine, _agent_updated("triage_agent")) + assert _types(events) == [EventType.STEP_STARTED] + assert events[0].step_name == "triage_agent" + closing = engine.finalize() + assert _types(closing) == [EventType.STEP_FINISHED] + assert closing[0].step_name == "triage_agent" + + +def test_handoff_pairs_step_finished_with_the_next_step_started(): + engine = SDKToAGUITranslator() + _drive(engine, _agent_updated("triage_agent")) + events = _drive(engine, _agent_updated("billing_agent")) + assert _types(events) == [EventType.STEP_FINISHED, EventType.STEP_STARTED] + assert events[0].step_name == "triage_agent" + assert events[1].step_name == "billing_agent" + + +# ── handoff items (surface as a tool call + result) ────────────────────── + + +def test_handoff_call_and_output_items_map_to_tool_call_and_result(): + engine = SDKToAGUITranslator() + call = HandoffCallItem( + agent=_AGENT, + raw_item=ResponseFunctionToolCall( + id="fc_h", + type="function_call", + call_id="call_h", + name="transfer_to_billing", + arguments="{}", + ), + ) + output = HandoffOutputItem( + agent=_AGENT, + raw_item={"type": "function_call_output", "call_id": "call_h", "output": "done"}, + source_agent=_AGENT, + target_agent=Agent(name="billing_agent"), + ) + call_events = _drive(engine, _run_item(call)) + assert _types(call_events) == [ + EventType.TOOL_CALL_START, + EventType.TOOL_CALL_ARGS, + EventType.TOOL_CALL_END, + ] + assert {e.tool_call_id for e in call_events} == {"call_h"} + assert call_events[0].tool_call_name == "transfer_to_billing" + + result_events = _drive(engine, _run_item(output)) + assert _types(result_events) == [EventType.TOOL_CALL_RESULT] + assert result_events[0].tool_call_id == "call_h" + assert result_events[0].content == "done" + + +# ── MCP approval → CUSTOM (no native AG-UI shape yet) ──────────────────── + + +def test_mcp_approval_request_maps_to_a_custom_event(): + engine = SDKToAGUITranslator() + item = MCPApprovalRequestItem( + agent=_AGENT, + raw_item=McpApprovalRequest( + id="mcpr_1", + type="mcp_approval_request", + name="do_it", + arguments="{}", + server_label="srv", + ), + ) + events = _drive(engine, _run_item(item)) + assert _types(events) == [EventType.CUSTOM] + assert events[0].name == "mcp_approval_request" + assert events[0].value["name"] == "do_it" + + +# ── graceful degradation ───────────────────────────────────────────────── + + +def test_unknown_stream_event_type_translates_to_nothing(): + engine = SDKToAGUITranslator() + assert engine.translate(SimpleNamespace(type="something_new", data=None)) == [] From 1387aa33cfa95dbd8de7ee2ca4921d46370f0bcc Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 11 Jul 2026 03:13:15 +0200 Subject: [PATCH 18/94] docs(openai-agents): append session notes on engine-mapping tests and translator-first docs --- integrations/openai-agents/python/README.md | 162 +++++++++++++------- 1 file changed, 110 insertions(+), 52 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index e19d47fa12..071ca7966e 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -44,55 +44,16 @@ For local development this package uses [uv](https://docs.astral.sh/uv/): uv sync ``` -## Quick Start: Serve an Agent +## Quick Start: Compose It Yourself (recommended) -The fastest path. Wrap a plain SDK `Agent` with `OpenAIAgentsAgent`, then hand -it to `add_openai_agents_fastapi_endpoint` — SSE, content negotiation, a -`/health` check, lifecycle events, and the state/messages snapshots are all -wired for you. - -```python -from agents import Agent -from fastapi import FastAPI - -from ag_ui_openai_agents import ( - OpenAIAgentsAgent, - add_openai_agents_fastapi_endpoint, -) - -agent = OpenAIAgentsAgent(Agent(name="assistant", instructions="Be concise.")) - -app = FastAPI() -add_openai_agents_fastapi_endpoint(app, agent, "/") -``` - -That's the whole integration. `OpenAIAgentsAgent`: - -- holds no per-request state — one instance serves every request; the SDK - `Agent` is a config template, and client-declared tools are merged onto a - per-request `clone()`, so concurrent requests never see each other's tools; -- takes `run_config=...` to route runs through a non-OpenAI provider (e.g. - LiteLLM) or set run-wide model settings; -- exposes `run(RunAgentInput) -> AsyncIterator[BaseEvent]` if you want to serve - it on a transport other than FastAPI. - -```python -from agents import RunConfig -from agents.extensions.models.litellm_provider import LitellmProvider - -agent = OpenAIAgentsAgent( - Agent(name="assistant", instructions="Be concise.", model="gemini/gemini-2.5-flash"), - run_config=RunConfig(model_provider=LitellmProvider()), -) -``` - -Need finer control (a custom transport, your own SSE framing, per-run branching)? -Drop to the translator — see the next section. - -## Quick Start: Compose It Yourself - -The lower-level path the wrapper is built on: accept `RunAgentInput`, run your -OpenAI agent with `Runner.run_streamed`, and stream AG-UI events back over SSE. +**This is the recommended way to use this integration.** `AGUITranslator` is +just an events translator — it converts at the AG-UI boundary and nothing +else. You keep full control of the agent (any `Agent` config, model, +handoffs, guardrails) and full control of the backend server (FastAPI or +anything else, your own routes, your own SSE/WebSocket framing, your own +session/auth logic). Nothing about your `Runner.run_streamed` call or your +server is owned by this package — accept `RunAgentInput`, run your OpenAI +agent normally, stream AG-UI events back: ```python from agents import Agent, Runner @@ -149,18 +110,66 @@ TEXT_MESSAGE_END -> RUN_FINISHED`. A full multi-demo server (chat, backend tools, human-in-the-loop, handoffs, orchestrator) lives in [`examples/`](examples/). +## Quick Start: Serve an Agent (opinionated shortcut) + +Want the boilerplate above wired up for you instead? Wrap a plain SDK `Agent` +with `OpenAIAgentsAgent`, then hand it to `add_openai_agents_fastapi_endpoint` +— SSE, content negotiation, a `/health` check, lifecycle events, and the +state/messages snapshots are all wired for you. Trades away the control the +translator gives you (agent config is fixed at construction, server is +FastAPI) for less code. + +```python +from agents import Agent +from fastapi import FastAPI + +from ag_ui_openai_agents import ( + OpenAIAgentsAgent, + add_openai_agents_fastapi_endpoint, +) + +agent = OpenAIAgentsAgent(Agent(name="assistant", instructions="Be concise.")) + +app = FastAPI() +add_openai_agents_fastapi_endpoint(app, agent, "/") +``` + +`OpenAIAgentsAgent`: + +- holds no per-request state — one instance serves every request; the SDK + `Agent` is a config template, and client-declared tools are merged onto a + per-request `clone()`, so concurrent requests never see each other's tools; +- takes `run_config=...` to route runs through a non-OpenAI provider (e.g. + LiteLLM) or set run-wide model settings; +- exposes `run(RunAgentInput) -> AsyncIterator[BaseEvent]` if you want to serve + it on a transport other than FastAPI. + +```python +from agents import RunConfig +from agents.extensions.models.litellm_provider import LitellmProvider + +agent = OpenAIAgentsAgent( + Agent(name="assistant", instructions="Be concise.", model="gemini/gemini-2.5-flash"), + run_config=RunConfig(model_provider=LitellmProvider()), +) +``` + +Need finer control (a custom transport, your own SSE framing, per-run +branching)? Use the translator directly — see the section above. + ## Public API Two layers, pick by how much control you want: | Name | Kind | Use it for | |---|---|---| +| `AGUITranslator` | translator (recommended) | compose it yourself — `to_sdk` + `to_agui`; full control of the agent and server | | `OpenAIAgentsAgent` | wrapper class | serve an agent: `run(RunAgentInput) -> AsyncIterator[BaseEvent]` | | `add_openai_agents_fastapi_endpoint(app, agent, path)` | helper | wire a wrapped agent to FastAPI (SSE + `/health`) | -| `AGUITranslator` | translator | compose it yourself — `to_sdk` + `to_agui` | -The wrapper is built on the translator; use the translator directly for custom -transports or per-run branching. +The wrapper is built on the translator; it trades control for less code. The +translator is just an events translator — it does not own your agent or your +server, so start there unless the wrapper's shortcut fits as-is. `AGUITranslator` pairs with the SDK's streaming run mode: @@ -175,7 +184,8 @@ fresh per-run engine it needs. Create one instance and share it. | Need | Use | |---|---| -| Just serve an agent over FastAPI | `OpenAIAgentsAgent` + `add_openai_agents_fastapi_endpoint` | +| Full control of the agent config and the server (recommended default) | `AGUITranslator` — compose it yourself | +| Just serve a fixed agent over FastAPI, no custom server logic | `OpenAIAgentsAgent` + `add_openai_agents_fastapi_endpoint` | | Live chat, tool-call progress, reasoning progress | `AGUITranslator` with `Runner.run_streamed` | | FastAPI SSE | Return `StreamingResponse(_stream(...), media_type="text/event-stream")` | | WebSocket or another async transport | Iterate `translator.to_agui(result, run_input)` and send each event JSON | @@ -344,6 +354,54 @@ The translator translates. Your run loop still owns orchestration: This keeps the integration framework-neutral. FastAPI, Starlette, Django, aiohttp, raw ASGI, WebSockets, or tests can all use the same translator calls. +### Event Mapping + +Both directions, source of truth is `engine/agui_to_sdk.py` and +`engine/sdk_to_agui.py`. + +**Inbound** — AG-UI `Message` → Responses-API input item (`AGUIToSDKTranslator`): + +| AG-UI type | SDK / Responses-API shape | +|---|---| +| `UserMessage` | `{"type": "message", "role": "user", "content": [...]}` (multimodal-aware) | +| `SystemMessage` | `{"type": "message", "role": "system", ...}` | +| `DeveloperMessage` | `{"type": "message", "role": "developer", ...}` | +| `AssistantMessage` | optional text `message` item + one `function_call` item per `tool_calls` entry | +| `ToolMessage` | `{"type": "function_call_output", "call_id": ..., "output": ...}` | +| `ReasoningMessage` | `{"type": "reasoning", ...}` if `encrypted_value` is set, else dropped (plaintext reasoning isn't replayable) | +| `ActivityMessage` | dropped (no Responses-API equivalent; override to fold into the prompt) | +| `Tool` (client-declared) | SDK `FunctionTool` proxy that raises `ClientToolPending` when invoked | +| Content parts: `TextInputContent`, `ImageInputContent`, `AudioInputContent`, `DocumentInputContent`, `BinaryInputContent` | `input_text` / `input_image` / `input_audio` / `input_file` parts | +| `VideoInputContent` | dropped (Responses API has no video input) | + +**Outbound** — SDK stream event → AG-UI `BaseEvent` (`SDKToAGUITranslator`): + +| SDK event / item | AG-UI event(s) | +|---|---| +| `response.output_item.added` (message) | `TEXT_MESSAGE_START` | +| `response.output_text.delta` / `response.refusal.delta` | `TEXT_MESSAGE_CONTENT` (lazy-opens the window if needed) | +| `response.output_item.done` (message) / `response.output_text.done` | `TEXT_MESSAGE_END` | +| `response.output_item.added` (function_call / hosted tool call) | `TOOL_CALL_START` | +| `response.function_call_arguments.delta` | `TOOL_CALL_ARGS` | +| `response.output_item.done` (function_call / hosted tool call) | `TOOL_CALL_END` | +| `ToolCallOutputItem` | `TOOL_CALL_RESULT` | +| `HandoffCallItem` | `TOOL_CALL_START` / `TOOL_CALL_ARGS` / `TOOL_CALL_END` (a handoff is a function call) | +| `HandoffOutputItem` | `TOOL_CALL_RESULT` | +| `response.reasoning_summary_text.delta` / `response.reasoning_text.delta` | `REASONING_START` (once) + `REASONING_MESSAGE_START` + `REASONING_MESSAGE_CONTENT` | +| `response.reasoning_summary_part.done` / `response.reasoning_text.done` | `REASONING_MESSAGE_END` | +| `ReasoningItem` (finished, with `encrypted_content`) | `REASONING_ENCRYPTED_VALUE` | +| stream end / any open reasoning when text or a tool call opens | `REASONING_END` | +| `AgentUpdatedStreamEvent` | `STEP_FINISHED` (previous agent, if any) + `STEP_STARTED` (new agent) | +| `MCPApprovalRequestItem` | `CUSTOM` (name=`"mcp_approval_request"`) | +| `MCPListToolsItem`, `MCPApprovalResponseItem` | dropped (server-side bookkeeping / echo) | +| stream start / end (always, via `to_agui`) | `RUN_STARTED` / `RUN_FINISHED` (or `RUN_ERROR`) | +| end of stream, `run_input` given (default) | `MESSAGES_SNAPSHOT` | +| `run_input.state`, if set | `STATE_SNAPSHOT` (echoed once by `to_agui`) | + +Unknown SDK event or item types translate to `[]` with a debug log — +graceful degradation, never a raise. See `tests/test_engine_mapping.py` for +the streaming behavior pinned event-by-event. + ### Guardrails The AG-UI protocol has no guardrail event type, so guardrails surface as From 98442c0bea2607adc1281140c4cebcb0df5d4342 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 11 Jul 2026 14:02:06 +0200 Subject: [PATCH 19/94] fix(translator): defer TEXT_MESSAGE_START until first real delta --- .../ag_ui_openai_agents/engine/agui_to_sdk.py | 26 +++++++-- .../ag_ui_openai_agents/engine/sdk_to_agui.py | 54 +++++++++++++++---- .../src/ag_ui_openai_agents/translator.py | 14 ++++- .../python/tests/test_engine_mapping.py | 52 +++++++++++++++++- 4 files changed, 132 insertions(+), 14 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py index 300d9feb3c..4a8288d558 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py @@ -31,6 +31,7 @@ VideoInputContent, ) from agents import FunctionTool, RunContextWrapper, TResponseInputItem +from agents.exceptions import AgentsException from .helpers import coerce_to_str, read_attr from .types import TranslatedInput @@ -42,7 +43,7 @@ # Sentinel exception used by client-tool proxies # --------------------------------------------------------------------------- -class ClientToolPending(Exception): +class ClientToolPending(AgentsException): """Raised by a client-tool proxy to signal "stop, the UI owns this call". The outer run loop catches it, cancels the SDK run after the current @@ -50,6 +51,14 @@ class ClientToolPending(Exception): next AG-UI request (which carries an AG-UI ToolMessage with the client's result) can resume from the same point. + Subclasses AgentsException deliberately: the SDK's own tool executor + (agents.run_internal.tool_execution._run_single_tool) special-cases + `isinstance(e, AgentsException)` to re-raise it as-is — anything else + gets wrapped in a generic UserError first, which would hide this from + the outer run loop's `except ClientToolPending` and turn every + client-owned tool call into a hard run failure instead of a clean + hand-off. + Args: tool_name: Name of the client-owned tool that was called. tool_call_id: The SDK's call_id for this invocation. @@ -312,9 +321,20 @@ def translate_assistant_message( if text: items.append( { - "type": "message", "role": "assistant", - "content": [{"type": "output_text", "text": text}], + "content": text, + # Deliberately no "type" key: the SDK's own item + # classifiers (agents.models.chatcmpl_converter, + # Converter.maybe_easy_input_message) match + # EasyInputMessageParam on the exact key set + # {"content", "role"} — adding "type": "message" here + # makes it match maybe_response_output_message instead + # (any dict with type="message"/role="assistant"), whose + # branch assumes content is a list of output_text/refusal + # parts and blows up on a plain string. This is a prior + # assistant turn being replayed as *input*, so the plain + # EasyInputMessageParam shape is exactly right — no id, + # no status, no content-part wrapping needed. } ) for tool_call in message.tool_calls or []: diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py index 05cb2ff04e..81188c76a5 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py @@ -100,6 +100,12 @@ def __init__(self) -> None: # otherwise __idx_. Value is the id we already sent in # the matching *_START event, so we can pair the END to it. self._open_texts: dict[str, str] = {} # key -> message_id + # A message item that was announced (output_item.added) but whose + # TEXT_MESSAGE_START we're holding back until a real delta arrives. + # Value is the id the START will carry once (if) it opens. Lets a + # text-less item — a pure tool-call turn on some providers — pass + # through without an empty text window bracketing the tool call. + self._pending_text_ids: dict[str, str] = {} # key -> deferred message_id self._open_tool_calls: dict[str, str] = {} # key -> tool_call_id self._open_reasonings: dict[str, str] = {} # key -> phase message_id self._open_reasoning_parts: dict[str, str] = {} # key -> part message_id @@ -326,7 +332,17 @@ def translate_output_item_added(self, data: Any) -> list[BaseEvent]: key = self._window_key(item_id, read_attr(data, "output_index")) if item_type == SDKItemType.MESSAGE: - return self._open_text(key, self._resolve_id(item_id, new_message_id)) + # Defer TEXT_MESSAGE_START until a real delta actually arrives. + # Some providers (LiteLLM's chat-completions adapter, for one) + # emit a message item even on a turn that ends up being a pure + # tool call with no spoken text — opening the window here would + # leave an empty TEXT_MESSAGE_START/END pair wrapping the tool + # call on the wire. Remember the real id so the lazy open still + # uses it. Output has begun, though, so close any open reasoning + # now (some backends send reasoning-done late) — that must not + # wait for the deferred text. + self._pending_text_ids[key] = self._resolve_id(item_id, new_message_id) + return self._close_all_reasonings() if item_type == SDKItemType.FUNCTION_CALL: call_id = read_attr(item, "call_id") or self._resolve_id(item_id, new_tool_call_id) name = read_attr(item, "name") or "" @@ -356,6 +372,11 @@ def translate_output_item_done(self, data: Any) -> list[BaseEvent]: key = self._window_key(read_attr(item, "id"), read_attr(data, "output_index")) if item_type == SDKItemType.MESSAGE: + # Drop a deferred-but-never-opened window: no delta ever arrived, + # so there's nothing on the wire to close and no empty window to + # emit. If a delta did arrive the pending entry is already gone + # and this pop is a no-op; _close_text then closes the real one. + self._pending_text_ids.pop(key, None) return self._close_text(key) if item_type == SDKItemType.FUNCTION_CALL or item_type in HOSTED_TOOL_CALL_TYPES: return self._close_tool_call(key) @@ -391,6 +412,9 @@ def translate_text_done(self, data: Any) -> list[BaseEvent]: The closing event, or [] if already closed. """ key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) + # A text-level done with no delta ever seen: drop the deferred window + # the same way output_item.done does, so it never opens empty. + self._pending_text_ids.pop(key, None) return self._close_text(key) def translate_refusal_delta(self, data: Any) -> list[BaseEvent]: @@ -549,16 +573,23 @@ def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEve # message, under the exact id that earlier close already used. self._record_text(key, text) return [] + # "new": nothing was ever streamed for this item. A real id also + # supersedes any deferred window we were holding for it. + self._pending_text_ids.pop(raw_id, None) + if not text: + # An empty assistant commit — emit no window (START+END with no + # content is exactly the empty-bubble artifact) and keep it out of + # the snapshot. + return [] message_id = self._resolve_id(raw_id, new_message_id) events = self._open_text(message_id, message_id) - if text: - events.append( - TextMessageContentEvent( - type=EventType.TEXT_MESSAGE_CONTENT, - message_id=message_id, - delta=text, - ) + events.append( + TextMessageContentEvent( + type=EventType.TEXT_MESSAGE_CONTENT, + message_id=message_id, + delta=text, ) + ) events.extend(self._close_text(message_id)) self._record_text(message_id, text) return events @@ -895,7 +926,12 @@ def _emit_text_content(self, key: str, delta: str) -> list[BaseEvent]: return [] events: list[BaseEvent] = [] if key not in self._open_texts: - events.extend(self._open_text(key, new_message_id())) + # First real delta for a deferred item opens the window now, under + # the id output_item.added reserved (falling back to a fresh id if + # the deltas arrived with no added first). + events.extend( + self._open_text(key, self._pending_text_ids.pop(key, None) or new_message_id()) + ) events.append( TextMessageContentEvent( type=EventType.TEXT_MESSAGE_CONTENT, diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index cdc4e7c52c..4bf0c85fcf 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -26,7 +26,7 @@ StateSnapshotEvent, ) -from .engine.agui_to_sdk import AGUIToSDKTranslator +from .engine.agui_to_sdk import AGUIToSDKTranslator, ClientToolPending from .engine.sdk_to_agui import SDKToAGUITranslator from .engine.types import TranslatedInput @@ -183,6 +183,18 @@ async def to_agui( yield event if emit_messages_snapshot: yield outbound.build_messages_snapshot(run_input) + except ClientToolPending: + # Not a failure — a client-declared tool's proxy raises this the + # instant the model calls it, specifically so the SDK stops here + # and hands the call back to the client. The TOOL_CALL_START/ + # ARGS/END trio for it was already yielded by the normal + # translate() dispatch above (the run-item event that names the + # call arrives before the SDK ever invokes it), so this is a + # clean end of turn: finalize any open windows, snapshot, finish. + for event in outbound.finalize(): + yield event + if emit_messages_snapshot: + yield outbound.build_messages_snapshot(run_input) except (Exception, asyncio.CancelledError) as exc: # asyncio.CancelledError is BaseException, not Exception (3.8+) — # a mid-stream cancellation (timeout, dropped connection) would diff --git a/integrations/openai-agents/python/tests/test_engine_mapping.py b/integrations/openai-agents/python/tests/test_engine_mapping.py index 32ab17aa75..10bf22f9ea 100644 --- a/integrations/openai-agents/python/tests/test_engine_mapping.py +++ b/integrations/openai-agents/python/tests/test_engine_mapping.py @@ -147,6 +147,48 @@ def test_refusal_delta_streams_into_the_text_window(): assert events[1].delta == "no" +def test_text_less_message_item_wrapping_a_tool_call_emits_no_window(): + # A provider (e.g. LiteLLM's chat-completions adapter) can announce a + # message item even on a pure tool-call turn that never carries text: an + # output_item.added(message), the whole tool call, then + # output_item.done(message) — with no text delta in between. The message + # window must never open, so the tool call stays a clean sibling and no + # empty TEXT_MESSAGE_START/END brackets it on the wire. + engine = SDKToAGUITranslator() + events = _drive( + engine, + _added(SDKItemType.MESSAGE, output_index=0, id="msg_1"), + _added( + SDKItemType.FUNCTION_CALL, + output_index=1, + id="fc_1", + call_id="call_1", + name="change_background", + ), + _raw( + RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + item_id="fc_1", + output_index=1, + delta='{"background":"red"}', + ), + _done( + SDKItemType.FUNCTION_CALL, + output_index=1, + id="fc_1", + call_id="call_1", + name="change_background", + ), + _done(SDKItemType.MESSAGE, output_index=0, id="msg_1"), + ) + assert _types(events) == [ + EventType.TOOL_CALL_START, + EventType.TOOL_CALL_ARGS, + EventType.TOOL_CALL_END, + ] + assert EventType.TEXT_MESSAGE_START not in _types(events) + assert EventType.TEXT_MESSAGE_END not in _types(events) + + # ── tool-call streaming ────────────────────────────────────────────────── @@ -248,12 +290,20 @@ def test_reasoning_auto_closes_when_text_output_starts(): delta="hmm", ), ) + # output_item.added closes reasoning right away (output has begun), but + # holds back TEXT_MESSAGE_START — that waits for a real delta so a + # text-less item can't emit an empty window. events = _drive(engine, _added(SDKItemType.MESSAGE, id="msg_1")) assert _types(events) == [ EventType.REASONING_MESSAGE_END, EventType.REASONING_END, - EventType.TEXT_MESSAGE_START, ] + events = _drive( + engine, + _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="ok"), + ) + assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] + assert events[0].message_id == "msg_1", "deferred START must carry the reserved id" # ── multi-agent steps ──────────────────────────────────────────────────── From 5b0dcfa44dd651e58fceca12bd0751e04ebb564e Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 11 Jul 2026 16:43:46 +0200 Subject: [PATCH 20/94] feat(openai-agents): add agent examples and configuration files --- .../python/examples/.env.example | 5 + .../openai-agents/python/examples/.gitignore | 3 + .../openai-agents/python/examples/README.md | 113 + .../examples/agents_examples/__init__.py | 53 + .../examples/agents_examples/agentic_chat.py | 19 + .../agents_examples/backend_tool_rendering.py | 49 + .../examples/agents_examples/constants.py | 15 + .../examples/agents_examples/handoff.py | 48 + .../agents_examples/human_in_the_loop.py | 44 + .../examples/agents_examples/orchestrator.py | 73 + .../tool_based_generative_ui.py | 38 + .../python/examples/pyproject.toml | 26 + .../openai-agents/python/examples/server.py | 108 + .../python/examples/translator_server.py | 169 ++ .../openai-agents/python/examples/uv.lock | 2170 +++++++++++++++++ 15 files changed, 2933 insertions(+) create mode 100644 integrations/openai-agents/python/examples/.env.example create mode 100644 integrations/openai-agents/python/examples/.gitignore create mode 100644 integrations/openai-agents/python/examples/README.md create mode 100644 integrations/openai-agents/python/examples/agents_examples/__init__.py create mode 100644 integrations/openai-agents/python/examples/agents_examples/agentic_chat.py create mode 100644 integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py create mode 100644 integrations/openai-agents/python/examples/agents_examples/constants.py create mode 100644 integrations/openai-agents/python/examples/agents_examples/handoff.py create mode 100644 integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py create mode 100644 integrations/openai-agents/python/examples/agents_examples/orchestrator.py create mode 100644 integrations/openai-agents/python/examples/agents_examples/tool_based_generative_ui.py create mode 100644 integrations/openai-agents/python/examples/pyproject.toml create mode 100644 integrations/openai-agents/python/examples/server.py create mode 100644 integrations/openai-agents/python/examples/translator_server.py create mode 100644 integrations/openai-agents/python/examples/uv.lock diff --git a/integrations/openai-agents/python/examples/.env.example b/integrations/openai-agents/python/examples/.env.example new file mode 100644 index 0000000000..2b26eefcf1 --- /dev/null +++ b/integrations/openai-agents/python/examples/.env.example @@ -0,0 +1,5 @@ +# OpenAI API Key (required) +OPENAI_API_KEY=sk-your-key-here + +# Optional: override the model used by every example agent +# OPENAI_DEFAULT_MODEL=gpt-4.1-mini diff --git a/integrations/openai-agents/python/examples/.gitignore b/integrations/openai-agents/python/examples/.gitignore new file mode 100644 index 0000000000..cff554307a --- /dev/null +++ b/integrations/openai-agents/python/examples/.gitignore @@ -0,0 +1,3 @@ +.env +__pycache__/ +*.pyc diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md new file mode 100644 index 0000000000..8c4d5ac0d5 --- /dev/null +++ b/integrations/openai-agents/python/examples/README.md @@ -0,0 +1,113 @@ +# OpenAI Agents SDK examples + +Runnable demos for `ag_ui_openai_agents`, one FastAPI route per agent. Two +servers, same demos and same output, showing the two ways to build: + +- **`translator_server.py`** — the translator by hand (`to_sdk` → + `Runner.run_streamed` → `to_agui`). **Recommended.** Full control of the + agent and the server; `AGUITranslator` is just an events translator. Every + step is visible; branch mid-run if you need to. +- **`server.py`** — the serve layer (`OpenAIAgentsAgent` + + `add_openai_agents_fastapi_endpoint`). One wrapped agent + one endpoint call + per demo, no run loop to get wrong — an opinionated shortcut that trades + control for less code. + +Model provider is **native OpenAI** +(`OPENAI_API_KEY`) — the translators are provider-agnostic, but these +examples exercise the plain-OpenAI path deliberately, since that's what most +integrators will run in production. (LiteLLM/Gemini and the +`FAKE_RESPONSES_ID` handling are covered by the drift-guard/unit tests, not +these examples.) + +## Running the server + +```bash +cd examples +uv sync +cp .env.example .env # fill in OPENAI_API_KEY +uv run python server.py +``` + +Server runs on **http://localhost:8022** (the port the AG-UI Dojo expects; +override with `PORT`). + +## Testing + +```bash +curl -N -X POST http://localhost:8022/agentic_chat \ + -H 'Content-Type: application/json' \ + -d '{ + "thread_id": "t1", + "run_id": "r1", + "messages": [{"id":"m1","role":"user","content":"Say hi in one sentence."}], + "tools": [], + "state": {}, + "context": [], + "forwarded_props": null + }' +``` + +Swap the path to hit a different demo — `GET /health` lists every registered +agent. Demos map 1:1 onto the AG-UI Dojo feature pages, plus two multi-agent +patterns (`handoff`, `orchestrator`). + +> The stateful demos (`shared_state`, `agentic_generative_ui`, +> `predictive_state_updates`) are shelved together with the `AGUIContext` +> state bridge — see `.dev/shelved/` in the package root. + +## Agents + +### `agentic_chat` +Plain conversation, no tools. Exercises `TEXT_MESSAGE_START/CONTENT/END` +only — the smallest possible smoke test. + +**Try:** `"Say hi in one sentence."` + +### `backend_tool_rendering` +A server-side `@function_tool` (`get_weather`) the SDK executes itself. +Exercises `TOOL_CALL_START/ARGS/END` + `TOOL_CALL_RESULT`. + +**Try:** `"What's the weather in Berlin?"` + +### `human_in_the_loop` +A *frontend*-owned tool (`generate_task_steps`) declared by the client in +`RunAgentInput.tools`, not the server. Uses the SDK's built-in +`tool_use_behavior=StopAtTools(...)` so the run ends the moment the model +calls it — the tool body never executes server-side. The frontend renders +the steps, gets user approval, and sends the result back as an ordinary +`ToolMessage` in the next request; no custom pause/resume state needed. + +**Try:** `"Plan a birthday party."` — requires a client that actually sends +a `generate_task_steps` tool definition in `RunAgentInput.tools` (e.g. the +AG-UI Dojo's human-in-the-loop page, which uses the same tool name). + +### `tool_based_generative_ui` +Another frontend-owned tool (`generate_haiku`), but here the tool call *is* +the deliverable: the frontend renders the haiku card straight from the +streamed `TOOL_CALL_ARGS` — no approval round-trip. Same `StopAtTools` +mechanics as `human_in_the_loop`. + +**Try:** `"Write me a haiku about the ocean."` — needs a client that sends a +`generate_haiku` tool definition (Dojo's tool-based generative UI page). + +### `handoff` +Multi-agent triage using the SDK's native `handoffs=`. A `triage_agent` +hands off to `billing_agent` or `refund_agent` depending on the request. +Exercises handoff call/result translation (`translate_handoff_call_item` / +`translate_handoff_output_item`) and per-agent `STEP_STARTED`/`STEP_FINISHED` +pairs (`translate_agent_updated_event`) so the client can attribute each +message to the agent that produced it. + +**Try:** `"I was charged twice for my last invoice."` (routes to +`billing_agent`) or `"I want a refund for order #1234."` (routes to +`refund_agent`). + +### `orchestrator` +Multi-agent via the SDK's **agents-as-tools** pattern (`Agent.as_tool()`) — +the complement to `handoff`: control never transfers, the orchestrator calls +`research_agent`, `writer_agent`, and `critic_agent` as tools and +synthesizes the final answer itself. Each specialist invocation appears to +the client as a normal `TOOL_CALL_*` + `TOOL_CALL_RESULT` sequence; the +nested agents' inner turns stay internal to the SDK. + +**Try:** `"Write a short piece about the history of coffee."` diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py new file mode 100644 index 0000000000..56e7a85362 --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -0,0 +1,53 @@ +"""Example agent factories for the AG-UI × OpenAI Agents SDK server. + +Each demo is a ``DemoConfig`` wrapping the SDK agent. ``build_registry()`` +assembles the full name → config map ``server.py`` serves, one route per key. + +Stateful demos (shared_state, agentic_generative_ui, +predictive_state_updates) are shelved together with ``AGUIContext`` — +see ``.dev/shelved/``. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from agents import Agent + +from .agentic_chat import create_agentic_chat_agent +from .backend_tool_rendering import create_backend_tool_agent +from .handoff import create_handoff_agent +from .human_in_the_loop import create_human_in_the_loop_agent +from .orchestrator import create_orchestrator_agent +from .tool_based_generative_ui import create_tool_based_generative_ui_agent + + +@dataclass(frozen=True) +class DemoConfig: + """One demo route: the agent to run for it.""" + + agent: Agent + + +def build_registry() -> dict[str, DemoConfig]: + """Assemble the demo registry served by ``server.py`` (one route per key).""" + return { + "agentic_chat": DemoConfig(agent=create_agentic_chat_agent()), + "backend_tool_rendering": DemoConfig(agent=create_backend_tool_agent()), + "human_in_the_loop": DemoConfig(agent=create_human_in_the_loop_agent()), + "tool_based_generative_ui": DemoConfig(agent=create_tool_based_generative_ui_agent()), + "handoff": DemoConfig(agent=create_handoff_agent()), + "orchestrator": DemoConfig(agent=create_orchestrator_agent()), + } + + +__all__ = [ + "DemoConfig", + "build_registry", + "create_agentic_chat_agent", + "create_backend_tool_agent", + "create_handoff_agent", + "create_human_in_the_loop_agent", + "create_orchestrator_agent", + "create_tool_based_generative_ui_agent", +] diff --git a/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py b/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py new file mode 100644 index 0000000000..25cfeb6bac --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py @@ -0,0 +1,19 @@ +"""Agentic chat — plain conversation, no tools. + +The baseline demo: exercises ``TEXT_MESSAGE_START/CONTENT/END`` only. Good +first smoke test before trying the tool / handoff examples. +""" + +from __future__ import annotations + +from agents import Agent + +from .constants import DEFAULT_MODEL + + +def create_agentic_chat_agent() -> Agent: + return Agent( + name="assistant", + model=DEFAULT_MODEL, + instructions="You are a helpful assistant. Be concise.", + ) diff --git a/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py b/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py new file mode 100644 index 0000000000..6743dbb1b0 --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py @@ -0,0 +1,49 @@ +"""Backend tool rendering — a server-side ``@function_tool``. + +Exercises ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` for a tool the +*backend* owns and executes (as opposed to :mod:`human_in_the_loop`, where the +frontend owns execution). The SDK runs the tool body itself; the translator +just reports the call and its result as they stream past. + +The dojo's weather card (apps/dojo .../backend_tool_rendering/page.tsx) reads +the tool result as a JSON object — temperature/conditions/humidity/ +wind_speed/feels_like — and the argument as `location`, matching every other +integration's version of this demo. A plain sentence string or a `city` +param renders as a blank/all-zero card, since the frontend has nothing to +parse. +""" + +from __future__ import annotations + +from agents import Agent, function_tool + +from .constants import DEFAULT_MODEL + +_WEATHER = { + "berlin": {"temperature": 24, "conditions": "sunny", "humidity": 40, "wind_speed": 12, "feels_like": 25}, + "munich": {"temperature": 22, "conditions": "cloudy", "humidity": 55, "wind_speed": 8, "feels_like": 21}, + "london": {"temperature": 17, "conditions": "rainy", "humidity": 75, "wind_speed": 18, "feels_like": 16}, + "paris": {"temperature": 26, "conditions": "sunny", "humidity": 35, "wind_speed": 10, "feels_like": 27}, + "new york": {"temperature": 19, "conditions": "clear", "humidity": 45, "wind_speed": 14, "feels_like": 18}, + "san francisco": {"temperature": 16, "conditions": "cloudy", "humidity": 68, "wind_speed": 20, "feels_like": 15}, + "tokyo": {"temperature": 23, "conditions": "clear", "humidity": 60, "wind_speed": 9, "feels_like": 24}, +} +_DEFAULT_WEATHER = {"temperature": 20, "conditions": "clear", "humidity": 50, "wind_speed": 10, "feels_like": 20} + + +@function_tool +def get_weather(location: str) -> dict: + """Get the current weather for a location.""" + return _WEATHER.get(location.lower(), _DEFAULT_WEATHER) + + +def create_backend_tool_agent() -> Agent: + return Agent( + name="weather_assistant", + model=DEFAULT_MODEL, + instructions=( + "You are a helpful weather assistant. Use the get_weather tool " + "whenever the user asks about weather in a specific location." + ), + tools=[get_weather], + ) diff --git a/integrations/openai-agents/python/examples/agents_examples/constants.py b/integrations/openai-agents/python/examples/agents_examples/constants.py new file mode 100644 index 0000000000..d61b9f5ea1 --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/constants.py @@ -0,0 +1,15 @@ +"""Shared constants for the example agents. + +These examples target **native OpenAI** as the model provider — the +translators are provider-agnostic (they key windows by wire id / ``call_id``, +never assume a specific vendor), but the reference examples exercise the +straightforward path: real ids, orderly ``output_item.done`` events, no +``FAKE_RESPONSES_ID`` placeholder juggling. Point ``DEFAULT_MODEL`` at any +Responses-API model your ``OPENAI_API_KEY`` has access to. +""" + +from __future__ import annotations + +import os + +DEFAULT_MODEL = os.getenv("OPENAI_DEFAULT_MODEL", "gpt-4.1-mini") diff --git a/integrations/openai-agents/python/examples/agents_examples/handoff.py b/integrations/openai-agents/python/examples/agents_examples/handoff.py new file mode 100644 index 0000000000..c2f8e88f25 --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/handoff.py @@ -0,0 +1,48 @@ +"""Handoff — multi-agent triage using the SDK's native ``handoffs=``. + +The triage agent hands the conversation to a specialist via the SDK's +built-in handoff mechanism (no custom routing code). Exercises: + +* ``translate_handoff_call_item`` / ``translate_handoff_output_item`` — the + handoff shows up as a AG-UI tool call + result, same as any other tool. +* ``translate_agent_updated_event`` — each hop emits ``STEP_FINISHED`` for + the outgoing agent and ``STEP_STARTED`` for the incoming one, so the + client can label which agent produced which message. +""" + +from __future__ import annotations + +from agents import Agent + +from .constants import DEFAULT_MODEL + +billing_agent = Agent( + name="billing_agent", + model=DEFAULT_MODEL, + instructions=( + "You handle billing questions: invoices, charges, payment methods. " + "Be precise and concise." + ), +) + +refund_agent = Agent( + name="refund_agent", + model=DEFAULT_MODEL, + instructions=( + "You handle refund requests. Ask for the order id if missing, then " + "confirm the refund amount and timeline." + ), +) + + +def create_handoff_agent() -> Agent: + return Agent( + name="triage_agent", + model=DEFAULT_MODEL, + instructions=( + "You triage customer support requests. Hand off to billing_agent " + "for billing questions, or refund_agent for refund requests. " + "Handle anything else yourself." + ), + handoffs=[billing_agent, refund_agent], + ) diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py new file mode 100644 index 0000000000..f1018e1fa0 --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py @@ -0,0 +1,44 @@ +"""Human-in-the-loop — a *frontend*-owned tool, approved before execution. + +Unlike :mod:`backend_tool_rendering`, ``generate_task_steps`` has no server +implementation — it arrives per-request as an AG-UI client tool +(``RunAgentInput.tools``), gets wrapped into an SDK ``FunctionTool`` proxy by +``AGUIToSDKTranslator.translate_tools()``, and is merged onto this agent by +the run loop (see ``server.py``). + +``tool_use_behavior=StopAtTools(...)`` is the SDK's built-in for "the model +may only *call* this tool, never execute it": the run ends the moment the +model emits the call, before the (dead-code) proxy body would ever run. The +frontend renders the steps for user approval, then sends the result back as +an AG-UI ``ToolMessage`` in the *next* request — ordinary multi-turn history, +no custom pause/resume machinery needed. +""" + +from __future__ import annotations + +from agents import Agent, StopAtTools + +from .constants import DEFAULT_MODEL + +# Must match the AG-UI client tool name the frontend declares in +# RunAgentInput.tools for this demo. +FRONTEND_TOOL_NAME = "generate_task_steps" + +INSTRUCTIONS = """You are a task planning assistant that breaks work into clear, actionable steps. + +When the user asks for help with a task: +1. Immediately call the `generate_task_steps` tool with an array of steps. + Each step is an object: {"description": "...", "status": "enabled"}. +2. Do not restate the steps as plain text — the frontend renders them. +3. After the call, wait for the user to approve/select steps; do not call + the tool again until they respond. +""" + + +def create_human_in_the_loop_agent() -> Agent: + return Agent( + name="task_planner", + model=DEFAULT_MODEL, + instructions=INSTRUCTIONS, + tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]), + ) diff --git a/integrations/openai-agents/python/examples/agents_examples/orchestrator.py b/integrations/openai-agents/python/examples/agents_examples/orchestrator.py new file mode 100644 index 0000000000..5606312d8d --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/orchestrator.py @@ -0,0 +1,73 @@ +"""Orchestrator — multi-agent via the SDK's agents-as-tools pattern. + +Complements :mod:`handoff`: there, control *transfers* to the specialist +(triage agent exits the conversation). Here the orchestrator stays in charge +and *calls* specialists as tools (``Agent.as_tool()``), possibly several in +one turn, then synthesizes their outputs itself. + +On the AG-UI side each specialist invocation is an ordinary +``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested +agent's own model turns stay internal to the SDK and are not streamed as +separate messages, so the client sees one coherent orchestrator transcript. +""" + +from __future__ import annotations + +from agents import Agent + +from .constants import DEFAULT_MODEL + +research_agent = Agent( + name="research_agent", + model=DEFAULT_MODEL, + instructions=( + "You research a topic and return the key facts as a short bullet " + "list. Facts only — no fluff, no conclusions." + ), +) + +writer_agent = Agent( + name="writer_agent", + model=DEFAULT_MODEL, + instructions=( + "You turn bullet-point facts into short, engaging prose. One tight " + "paragraph unless asked otherwise." + ), +) + +critic_agent = Agent( + name="critic_agent", + model=DEFAULT_MODEL, + instructions=( + "You review a draft and return concrete improvement suggestions as a " + "numbered list. Max 3 suggestions, be specific." + ), +) + + +def create_orchestrator_agent() -> Agent: + return Agent( + name="orchestrator", + model=DEFAULT_MODEL, + instructions=( + "You orchestrate a small content team. For writing requests: " + "call research_topic for the facts, then write_prose to draft, " + "then critique_draft to review, then produce the final version " + "yourself incorporating the critique. For simple questions, " + "answer directly without the team." + ), + tools=[ + research_agent.as_tool( + tool_name="research_topic", + tool_description="Research a topic and return key facts as bullets.", + ), + writer_agent.as_tool( + tool_name="write_prose", + tool_description="Turn bullet-point facts into short prose.", + ), + critic_agent.as_tool( + tool_name="critique_draft", + tool_description="Review a draft and suggest up to 3 improvements.", + ), + ], + ) diff --git a/integrations/openai-agents/python/examples/agents_examples/tool_based_generative_ui.py b/integrations/openai-agents/python/examples/agents_examples/tool_based_generative_ui.py new file mode 100644 index 0000000000..79ba38025a --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/tool_based_generative_ui.py @@ -0,0 +1,38 @@ +"""Tool-based generative UI — frontend tool renders the content. + +Like :mod:`human_in_the_loop`, the tool (``generate_haiku``) is *client*-owned: +it arrives per-request in ``RunAgentInput.tools``, gets wrapped into an SDK +``FunctionTool`` proxy, and ``StopAtTools`` ends the run the moment the model +calls it. The difference is intent: here the tool call *is* the deliverable — +the frontend renders the haiku card from the streamed ``TOOL_CALL_ARGS``, +no approval round-trip expected. +""" + +from __future__ import annotations + +from agents import Agent, StopAtTools + +from .constants import DEFAULT_MODEL + +# Must match the AG-UI client tool name the frontend declares in +# RunAgentInput.tools for this demo. +FRONTEND_TOOL_NAME = "generate_haiku" + +INSTRUCTIONS = """You are a creative writing assistant that renders haikus with a UI component. + +When the user asks for a haiku: +1. Immediately call the `generate_haiku` tool with the haiku data + (Japanese lines, English lines, and any other fields the tool declares). +2. Do NOT write the haiku as plain text — the frontend renders it. + +For non-creative requests, respond normally without the tool. +""" + + +def create_tool_based_generative_ui_agent() -> Agent: + return Agent( + name="haiku_assistant", + model=DEFAULT_MODEL, + instructions=INSTRUCTIONS, + tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]), + ) diff --git a/integrations/openai-agents/python/examples/pyproject.toml b/integrations/openai-agents/python/examples/pyproject.toml new file mode 100644 index 0000000000..1b1f4b0e51 --- /dev/null +++ b/integrations/openai-agents/python/examples/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "ag-ui-openai-agents-examples" +version = "0.1.0" +description = "Example usage of the AG-UI × OpenAI Agents SDK integration" +license = "MIT" +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "ag-ui-openai-agent-sdk", + "ag-ui-protocol>=0.1.18", + "fastapi>=0.115.2", + "uvicorn[standard]>=0.35.0", +] + +[tool.uv.sources] +ag-ui-openai-agent-sdk = { path = "../", editable = true } + +[project.scripts] +dev = "server:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["agents_examples"] diff --git a/integrations/openai-agents/python/examples/server.py b/integrations/openai-agents/python/examples/server.py new file mode 100644 index 0000000000..48f66b6d3f --- /dev/null +++ b/integrations/openai-agents/python/examples/server.py @@ -0,0 +1,108 @@ +""" +Multi-agent example server — the wrapper style (an opinionated shortcut). + +Built with the package's serve layer: each demo is one OpenAIAgentsAgent + one +add_openai_agents_fastapi_endpoint call; the wrapper does to_sdk / +Runner.run_streamed / to_agui / SSE / lifecycle for you. Trades away the +control translator_server.py gives you (agent config fixed at construction, +server is FastAPI) for less code. + +Compare with translator_server.py (the translator by hand, recommended) to +see the trade: this file is shorter and has no run loop to get wrong; +translator_server.py shows every step and lets you branch mid-run. + + POST /agentic_chat ← plain conversation + POST /backend_tool_rendering ← server-executed @function_tool + POST /human_in_the_loop ← frontend-owned tool, StopAtTools + POST /tool_based_generative_ui ← frontend tool renders the content + POST /handoff ← multi-agent triage via handoffs= + POST /orchestrator ← multi-agent via agents-as-tools + + GET /health ← liveness check (lists all demos) + GET //health ← per-demo check + +Run: + OPENAI_API_KEY=sk-... uv run python server.py + + # Auto-restart on code changes (this file, agents_examples/, or the + # package's own src/ — an editable install, so plain `uvicorn.run(app)` + # never notices those edits on its own): + RELOAD=1 OPENAI_API_KEY=sk-... uv run python server.py + +Test: + curl -N -X POST http://localhost:8022/agentic_chat \\ + -H 'Content-Type: application/json' \\ + -d '{ + "thread_id": "t1", + "run_id": "r1", + "messages": [{"id":"m1","role":"user","content":"Say hi in one sentence."}], + "tools": [], + "state": {}, + "context": [], + "forwarded_props": null + }' + +Expected event order for that request: + RUN_STARTED → STATE_SNAPSHOT → TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT (×N) + → TEXT_MESSAGE_END → MESSAGES_SNAPSHOT → RUN_FINISHED +""" + +from __future__ import annotations + +import logging +import os +from pathlib import Path + +import uvicorn +from fastapi import FastAPI + +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint + +from agents_examples import DemoConfig, build_registry + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +DEMOS: dict[str, DemoConfig] = build_registry() + +app = FastAPI(title="AG-UI × OpenAI Agents SDK examples (wrapper)") + +# The whole run loop: one wrapped agent + one endpoint per demo. +for demo_name, demo in DEMOS.items(): + add_openai_agents_fastapi_endpoint( + app, + OpenAIAgentsAgent(demo.agent, name=demo_name), + f"/{demo_name}", + ) + + +@app.get("/health") +async def health() -> dict: + return {"status": "ok", "agents": list(DEMOS)} + + +def main() -> int: + if not os.getenv("OPENAI_API_KEY"): + print("Error: OPENAI_API_KEY required") + return 1 + # 8022 is the port the AG-UI Dojo expects for this integration + # (apps/dojo/src/env.ts — OPENAI_AGENTS_PYTHON_URL). + port = int(os.getenv("PORT", "8022")) + host = os.getenv("HOST", "0.0.0.0") + print(f"Starting server on port {port} — agents: {list(DEMOS)}") + if os.getenv("RELOAD"): + uvicorn.run( + "server:app", + host=host, + port=port, + reload=True, + reload_dirs=[str(Path(__file__).parent), str(Path(__file__).parent.parent / "src")], + log_level="info", + ) + else: + uvicorn.run(app, host=host, port=port, log_level="info") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py new file mode 100644 index 0000000000..36856e761e --- /dev/null +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -0,0 +1,169 @@ +""" +Multi-agent example server — the translator by hand. Recommended. + +Wires the translator directly — to_sdk, Runner.run_streamed, to_agui — so the +full run loop is visible in one place. AGUITranslator is just an events +translator: this file keeps full control of the agent and the server. Same +demos and output as server.py, which builds the same thing with the +OpenAIAgentsAgent + add_openai_agents_fastapi_endpoint serve layer — an +opinionated shortcut that trades that control for less code. + +One FastAPI route per demo, all sharing the same run loop: + + POST /agentic_chat ← plain conversation + POST /backend_tool_rendering ← server-executed @function_tool + POST /human_in_the_loop ← frontend-owned tool, StopAtTools + POST /tool_based_generative_ui ← frontend tool renders the content + POST /handoff ← multi-agent triage via handoffs= + POST /orchestrator ← multi-agent via agents-as-tools + + GET /health ← liveness check + +Stateful demos (shared_state, agentic_generative_ui, +predictive_state_updates) are shelved with ``AGUIContext`` — see +``.dev/shelved/``. + +Run: + OPENAI_API_KEY=sk-... uv run python translator_server.py + + # Auto-restart on code changes (this file, agents_examples/, or the + # package's own src/ — an editable install, so plain `uvicorn.run(app)` + # never notices those edits on its own): + RELOAD=1 OPENAI_API_KEY=sk-... uv run python translator_server.py + +Test: + curl -N -X POST http://localhost:8022/agentic_chat \\ + -H 'Content-Type: application/json' \\ + -d '{ + "thread_id": "t1", + "run_id": "r1", + "messages": [{"id":"m1","role":"user","content":"Say hi in one sentence."}], + "tools": [], + "state": {}, + "context": [], + "forwarded_props": null + }' + +Expected event order for that request: + RUN_STARTED → STATE_SNAPSHOT → TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT (×N) + → TEXT_MESSAGE_END → MESSAGES_SNAPSHOT → RUN_FINISHED +""" + +from __future__ import annotations + +import logging +import os +from pathlib import Path + +import uvicorn +from agents import Runner +from ag_ui.core import RunAgentInput +from ag_ui.encoder import EventEncoder +from fastapi import FastAPI, HTTPException +from fastapi.responses import StreamingResponse + +from ag_ui_openai_agents import AGUITranslator + +from agents_examples import DemoConfig, build_registry + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Demo registry — one entry per demo, keyed by URL path +# --------------------------------------------------------------------------- + +DEMOS: dict[str, DemoConfig] = build_registry() + +# The translator is stateless/reusable — one instance serves every request; each +# to_agui call creates the fresh per-run engine it needs internally. +translator = AGUITranslator() + +# AG-UI's own SSE encoder: one `data: \n\n` frame per event. +encoder = EventEncoder() + +# --------------------------------------------------------------------------- +# FastAPI app +# --------------------------------------------------------------------------- + +app = FastAPI(title="AG-UI × OpenAI Agents SDK examples") + + +@app.get("/health") +async def health() -> dict: + return {"status": "ok", "agents": list(DEMOS)} + + +@app.post("/{agent_name}") +async def run(agent_name: str, body: RunAgentInput) -> StreamingResponse: + """Accept a RunAgentInput, run the named demo agent, stream AG-UI events back via SSE.""" + demo = DEMOS.get(agent_name) + if demo is None: + raise HTTPException(status_code=404, detail=f"Unknown agent: {agent_name!r}") + return StreamingResponse(_stream(demo, body), media_type=encoder.get_content_type()) + + +# --------------------------------------------------------------------------- +# Core streaming logic — shared by every demo in the registry +# --------------------------------------------------------------------------- + + +async def _stream(demo: DemoConfig, body: RunAgentInput): + """ + Translate the AG-UI input → run the SDK agent → translate events back. + + Each yielded chunk is one SSE line: ``data: \\n\\n`` + """ + # 1 — Translate AG-UI input into SDK-ready shapes. + translated = translator.to_sdk(body) + + # Frontend (client-owned) tools declared on this request — e.g. the + # human_in_the_loop demo's `generate_task_steps`. Merged per-request + # rather than baked into the Agent, since they come from the wire. + agent = demo.agent + if translated.tools: + agent = agent.clone(tools=[*agent.tools, *translated.tools]) + + # 2 — Run the agent; to_agui() wraps the stream with the lifecycle events + # (RUN_STARTED first, RUN_FINISHED / RUN_ERROR last), echoes the state + # snapshot, handles window bookkeeping + the final flush, and appends a + # trailing MESSAGES_SNAPSHOT. Nothing to hand-emit here. + try: + result = Runner.run_streamed(agent, input=translated.messages) + async for ag_event in translator.to_agui(result, body): + yield encoder.encode(ag_event) + except Exception: + # to_agui already emitted RUN_ERROR before re-raising; just log here so + # the real traceback lands in the server logs. + logger.exception("Agent run failed") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> int: + if not os.getenv("OPENAI_API_KEY"): + print("Error: OPENAI_API_KEY required") + return 1 + # 8022 is the port the AG-UI Dojo expects for this integration + # (apps/dojo/src/env.ts — OPENAI_AGENTS_PYTHON_URL). + port = int(os.getenv("PORT", "8022")) + host = os.getenv("HOST", "0.0.0.0") + print(f"Starting server on port {port} — agents: {list(DEMOS)}") + if os.getenv("RELOAD"): + uvicorn.run( + "translator_server:app", + host=host, + port=port, + reload=True, + reload_dirs=[str(Path(__file__).parent), str(Path(__file__).parent.parent / "src")], + log_level="info", + ) + else: + uvicorn.run(app, host=host, port=port, log_level="info") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/integrations/openai-agents/python/examples/uv.lock b/integrations/openai-agents/python/examples/uv.lock new file mode 100644 index 0000000000..7ca556581f --- /dev/null +++ b/integrations/openai-agents/python/examples/uv.lock @@ -0,0 +1,2170 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", + "python_full_version < '3.10'", +] + +[[package]] +name = "ag-ui-openai-agent-sdk" +version = "0.1.0" +source = { editable = "../" } +dependencies = [ + { name = "ag-ui-protocol" }, + { name = "fastapi", version = "0.128.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fastapi", version = "0.139.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "openai-agents", version = "0.8.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "openai-agents", version = "0.17.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "ag-ui-protocol", specifier = ">=0.1.18" }, + { name = "fastapi", specifier = ">=0.115.12" }, + { name = "openai-agents", specifier = ">=0.8.4" }, + { name = "pydantic", specifier = ">=2.13.4" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0" }] + +[[package]] +name = "ag-ui-openai-agents-examples" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "ag-ui-openai-agent-sdk" }, + { name = "ag-ui-protocol" }, + { name = "fastapi", version = "0.128.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fastapi", version = "0.139.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] +requires-dist = [ + { name = "ag-ui-openai-agent-sdk", editable = "../" }, + { name = "ag-ui-protocol", specifier = ">=0.1.18" }, + { name = "fastapi", specifier = ">=0.115.2" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" }, +] + +[[package]] +name = "ag-ui-protocol" +version = "0.1.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/10/4ad299267a7d04b89935aa99eef62979758fcf95aee9f8bb5d70c35b1be1/ag_ui_protocol-0.1.19.tar.gz", hash = "sha256:43c27f60d41712dcad0e9e0a203cbdf1c8e248b22417374c5c68321c448af4ea", size = 10720, upload-time = "2026-06-02T17:26:15.627Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/0a/bcad8116eb058e4b4a305e3fc37ebd7efc879deeb86b854f1c5b8b6e97dd/ag_ui_protocol-0.1.19-py3-none-any.whl", hash = "sha256:898843b1410d378824da0c6a776486288b9c5828689d0bf563118868e37f390f", size = 13490, upload-time = "2026-06-02T17:26:16.313Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, + { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, +] + +[[package]] +name = "fastapi" +version = "0.139.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" }, +] + +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "griffelib" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b9/be66eb0decd730d89b9c94f930e4b8d87787b05724bb84af98bfd825f72c/httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826", size = 208805, upload-time = "2026-05-25T22:16:50.434Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f7/b4d41eaae2869d31356bc4bbf546f44fae83ff298af0a043ca0625b06773/httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77", size = 113527, upload-time = "2026-05-25T22:16:51.672Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e4/77487e14fc7be47180fd0eb4267c7486d0cc59b74031839a3daf8650136b/httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4", size = 450035, upload-time = "2026-05-25T22:16:53.313Z" }, + { url = "https://files.pythonhosted.org/packages/da/72/5a8f787e323f56fbd86c32a4be92a86776e4cfe8b4317db999f452028362/httptools-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb", size = 451101, upload-time = "2026-05-25T22:16:54.696Z" }, + { url = "https://files.pythonhosted.org/packages/ed/41/b44a25560955197674b6744cb903664300e239235a5eaa69df0890d87054/httptools-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813", size = 436140, upload-time = "2026-05-25T22:16:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/74/b0/054aac84c03d7e097bf4c605fb7e74eec3d65c0276adf64ee97f3a103ff5/httptools-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba", size = 437041, upload-time = "2026-05-25T22:16:57.716Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e8/86b85bbc0ac7892232f1a99ab96a9aa71936984fa06adfc0afc83ca7789e/httptools-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557", size = 90454, upload-time = "2026-05-25T22:16:58.871Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/707b05d0a75f22ab301ff2660ebd4c2567cb13496ce5c277cafe8fa847a7/httptools-0.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8", size = 210820, upload-time = "2026-05-25T22:17:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/05/5b/1f9b7462464294db5d0b4e0fcb285c2f8233fb29ce48141c26b40fd505f3/httptools-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5", size = 114585, upload-time = "2026-05-25T22:17:41.314Z" }, + { url = "https://files.pythonhosted.org/packages/8a/52/037b6e734eaf5395d552fdc7459b7d0affaa33df07c5c6c7e02d60f6331c/httptools-0.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b", size = 451355, upload-time = "2026-05-25T22:17:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/8d09dcac561cd23050133e887b219e9361be9f547d3616db66b5857ed91a/httptools-0.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72", size = 452250, upload-time = "2026-05-25T22:17:43.911Z" }, + { url = "https://files.pythonhosted.org/packages/17/c5/c11ac814a89052dc0dba5ff99009f447e2e46ddb37eaa72d24079675ee9e/httptools-0.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005", size = 437132, upload-time = "2026-05-25T22:17:44.95Z" }, + { url = "https://files.pythonhosted.org/packages/35/e4/33ebdb8acb9650661966b3ca5687158122bf43c48207747afcc0245f66d8/httptools-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247", size = 438395, upload-time = "2026-05-25T22:17:46.465Z" }, + { url = "https://files.pythonhosted.org/packages/06/f6/e0577ea0f86af402772f363c7f9ba321c9ed8c760d223749c51365b162e2/httptools-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62", size = 91214, upload-time = "2026-05-25T22:17:47.61Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" }, + { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" }, + { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/5e/60/489a9df48730f05cfe4fb71ed2ed805264f60d3b0bbc65816f1cd8a4372a/jiter-0.16.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d8f80521644426d451e70f00c7974240cab8f6ee088aedaa9af2697153ab7805", size = 312477, upload-time = "2026-06-29T13:04:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/69/79/7b84250429f0e262b935c627a8522cab31b5303e617400b14e808b599bdb/jiter-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b21b412b899fd8bd51a3046934b59a3bb068b79f70a5c6010053ac77cc53f0c", size = 315397, upload-time = "2026-06-29T13:04:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/14/d2/a52aa9786bbec7e85547f041efec59b517488c396958cbf432d703898a73/jiter-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0758ab7747a984797cf048e8eedea1d8ef39d7994b25611daf5b48fc903e8873", size = 344340, upload-time = "2026-06-29T13:04:42.96Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f0/f33bfac2598a7e256e994b02444835ddd934226df7b022ae588c536131ec/jiter-0.16.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ec553a99b0987efd7a3645a1a825cf29c224e494db267a83369fcc8da9aeda5", size = 368451, upload-time = "2026-06-29T13:04:44.451Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f1/7fd3bd9378ff4366032a17fc82f562dda8aa92c491451705dc8c35275683/jiter-0.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3bd327cdfa118bc1ce69c214c2678571d5bd39b8ccd0ebf43a54db00541ba9a", size = 466402, upload-time = "2026-06-29T13:04:45.877Z" }, + { url = "https://files.pythonhosted.org/packages/ab/14/6d9948c7da0d56fe3a786a950b0f4a03139160e59238f2327361d6dfbda7/jiter-0.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26d122613ada2b708eb714695446f40fce5bdf2edb4b02116dec62faa62dfab3", size = 377987, upload-time = "2026-06-29T13:04:47.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c0/7acc77d44050cb3cc7ea4ddb32cc46614ba705876857973ebc2f008b1383/jiter-0.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e03a5f21a5ce96a9441b8cb32719a8b88ed5388f53e0f339c5bcf54f1317f9d0", size = 350341, upload-time = "2026-06-29T13:04:48.936Z" }, + { url = "https://files.pythonhosted.org/packages/26/a9/36e0d4af0f414ad054f6619f48ac7d607f42cb98f1672c39f964630556d0/jiter-0.16.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:a5c54ef4ff776d9675837ef535b3308d6e31c208d43ebc44a0f7ab8a208c68f7", size = 359924, upload-time = "2026-06-29T13:04:50.608Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ecbe98e370db5f8196ecbdae34806d369a0df95404de1562eb8c68415985/jiter-0.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1e7923093a376d93c6eb507c77045ae258d689ba577392846a1b3f10d0b09a9", size = 397728, upload-time = "2026-06-29T13:04:52.336Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1d/eb4261cdf94d4a485a47e3544ff1ee4e8a752b24aedeae250dc00a238166/jiter-0.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2a0d46ef67cc58d906a6132dd3040ca70ae4f0b0d7c9c052fe432c658a69b3f6", size = 524713, upload-time = "2026-06-29T13:04:53.864Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c3/a7dd14c86bcd04e7b0d5d13937e980f94085b61c0eec12622eaa159965c4/jiter-0.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:70a490b55634dc0d2606ce8a8e01b1d62459011beb368d15d76e1eaf62460e3d", size = 555324, upload-time = "2026-06-29T13:04:55.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0a/a10f28b2038bc3492de03ad6b7126a678d9f9f928ae72968e0cc2427bdad/jiter-0.16.0-cp39-cp39-win32.whl", hash = "sha256:9acf1b2faec82d998811ecce7ae84d9005e53410773e9d37d61cdc424ba4581b", size = 209348, upload-time = "2026-06-29T13:04:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/9b6377e067ebb0cde1f751e28cab26b09d4c8809050935196ee32525e2c9/jiter-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:491e7d072a253b156fff46b78bceac4652a697aa8d7082c9c18c03d7b7917d24", size = 202012, upload-time = "2026-06-29T13:04:58.836Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, + { name = "referencing", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "mcp" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-settings", marker = "python_full_version >= '3.10'" }, + { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", marker = "python_full_version >= '3.10'" }, + { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, +] + +[[package]] +name = "openai" +version = "2.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/f5/7c7cb955305cb41f7f3c5fd7e0e38bf6bbf2658468863d4b7b868a5cb8df/openai-2.44.0.tar.gz", hash = "sha256:68a5a5ffad82b8ff7d451c437529fb64f7c3b8123aaf0c021966a882d9e3947d", size = 988753, upload-time = "2026-06-24T20:56:02.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/f4/561ed79fd94876160018a5e75254cfcb9b0e62d4dded9dcb20072e86d623/openai-2.44.0-py3-none-any.whl", hash = "sha256:0a2a3ab2e29aeda368700f662ff9ba0f9df17ba4c54577a64e08b8115a3cc0ad", size = 1366216, upload-time = "2026-06-24T20:55:58.882Z" }, +] + +[[package]] +name = "openai-agents" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "griffe", marker = "python_full_version < '3.10'" }, + { name = "openai", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "types-requests", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/e0/9fa9eac9baf2816bc63cee28967d35a7ed9dc2f25e9fd2004f48ed6c8820/openai_agents-0.8.4.tar.gz", hash = "sha256:5d4c4861aedd56a82b15c6ddf6c53031a39859a222f08bbd5645d5967efa05e8", size = 2389744, upload-time = "2026-02-11T19:14:30.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/dc/10df015aebb0797a8367aab65200ac4f5221df20bbae76930f5b6ac8e001/openai_agents-0.8.4-py3-none-any.whl", hash = "sha256:2383c6e8e59ed4146b89d1b6f53e34e55caf94bc14ae3fd704e7aad5021f4ff1", size = 380662, upload-time = "2026-02-11T19:14:28.864Z" }, +] + +[[package]] +name = "openai-agents" +version = "0.17.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "griffelib", marker = "python_full_version >= '3.10'" }, + { name = "mcp", marker = "python_full_version >= '3.10'" }, + { name = "openai", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/b2/235cbfdefe86623fc77f65fc1d016686372f61d4a1bf3fc66151de2eb847/openai_agents-0.17.7.tar.gz", hash = "sha256:ca76e7f882c9d8f06e3dfb8064cc33bcb5a5f34a29816cb9af863f395964ff0c", size = 5485068, upload-time = "2026-06-24T05:15:33.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/2e/2e96ca6928951fe1d16744c22dc1355eda1dd5b0dd920ca1d3ab602929f8/openai_agents-0.17.7-py3-none-any.whl", hash = "sha256:51b5ae43756eea37032e430f95979ba3999af6b1ade397df6c0ffeaf1939646a", size = 856074, upload-time = "2026-06-24T05:15:31.741Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/5d/00/13a0c039569d1e583779ee1b8d7df6bfe275a0db83fcae14f01d6856c16e/pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae", size = 2115337, upload-time = "2026-05-06T13:38:37.741Z" }, + { url = "https://files.pythonhosted.org/packages/41/60/e70fa1ee03e243bdfd4b1fddf1e1f2a8fba681df3034b51b9376c0fb5bf5/pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201", size = 1957976, upload-time = "2026-05-06T13:37:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/11/9a/78fb5f2ea849f767ea802de8b4e8f5a0c4a48ddbe4bc66bd19ac2f55a01c/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0", size = 1979390, upload-time = "2026-05-06T13:36:52.419Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7d/3acfdcd000bad9735de0430a88355948469781f62cb841fd63e8a307e80e/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15", size = 2043263, upload-time = "2026-05-06T13:39:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/1325e5a8d7f9697416481c7f7c1c304738d6b961a7fd1ea0f054ce0f14fb/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76", size = 2225708, upload-time = "2026-05-06T13:40:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9ec8c38f33b26db0b612cb7fd165bb0a370773710432a2a74fa31287b430/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49", size = 2288494, upload-time = "2026-05-06T13:38:00.091Z" }, + { url = "https://files.pythonhosted.org/packages/65/05/497446a9586d1b2d24ee25ebe208beb15388f1875d783e1e014055d150ac/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928", size = 2095629, upload-time = "2026-05-06T13:38:23.632Z" }, + { url = "https://files.pythonhosted.org/packages/93/d9/cd5fa98f9d94f9294c15459396c8a2383c164469e679ac178d6d42cfee6b/pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066", size = 2119309, upload-time = "2026-05-06T13:39:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/20/1b/64cec655451ddbf3976df5dc9706b240df4fdaebdeebeadd4f59a8dab926/pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6", size = 2170216, upload-time = "2026-05-06T13:39:14.561Z" }, + { url = "https://files.pythonhosted.org/packages/2a/21/fe9f039138c9ea3be10ccdb6ec490acb54dcbef5a5e96dbdf1411f82b929/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9", size = 2186726, upload-time = "2026-05-06T13:37:51.597Z" }, + { url = "https://files.pythonhosted.org/packages/44/cb/19ca0da64821d1aefcef65f253aa9ecbdd0dde360f607d0f9b3d95db2b4e/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29", size = 2320400, upload-time = "2026-05-06T13:39:36.29Z" }, + { url = "https://files.pythonhosted.org/packages/cd/14/fe3fbf6e845bf2080dc2f282d75085ddf79d037b35634ecde68f33c217b4/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9", size = 2363318, upload-time = "2026-05-06T13:38:53.039Z" }, + { url = "https://files.pythonhosted.org/packages/62/88/60b110889507a426eecf626f7536566cb290ada71147eff49b6e2724ca62/pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1", size = 1988880, upload-time = "2026-05-06T13:39:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d6/8ede2f98f17e1e4e127d37be0eced4eee931a511c62cd68af50e1b25bfa9/pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac", size = 2079257, upload-time = "2026-05-06T13:39:38.498Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, + { url = "https://files.pythonhosted.org/packages/56/a0/f4a082a0232aaa7d0db2fe78b3e3ce03477ff8231c861b4405932b401001/pywin32-312-cp39-cp39-win32.whl", hash = "sha256:d620900033cc7531e50727c3c8333091df5dd3ffe6d68cdca38c03f5821408d5", size = 6360761, upload-time = "2026-06-04T07:49:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1f/d462540ddfccfea5ffe67ced40a99776f21d817fe6aeed5ab2cd87c1d926/pywin32-312-cp39-cp39-win_amd64.whl", hash = "sha256:dc90147579a905b8635e1b0ec6514967dcb07e6e0d9c42f1477feef14cac23bb", size = 6926456, upload-time = "2026-06-04T07:49:09.623Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6f/45e0e1d4078944440e7ee6fe003a39e412d53ab8a1772f0c3f4b2467b34c/pywin32-312-cp39-cp39-win_arm64.whl", hash = "sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc", size = 4306706, upload-time = "2026-06-04T07:49:11.793Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, +] + +[[package]] +name = "starlette" +version = "0.49.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "tqdm" +version = "4.68.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.39.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "h11", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/4f/f9fdac7cf6dd79790eb165639b5c452ceeabc7bbabbba4569155470a287d/uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302", size = 82001, upload-time = "2025-12-21T13:05:17.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/25/db2b1c6c35bf22e17fe5412d2ee5d3fd7a20d07ebc9dac8b58f7db2e23a0/uvicorn-0.39.0-py3-none-any.whl", hash = "sha256:7beec21bd2693562b386285b188a7963b06853c0d006302b3e4cfed950c9929a", size = 68491, upload-time = "2025-12-21T13:05:16.291Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "httptools", marker = "python_full_version < '3.10'" }, + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "uvloop", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles", version = "1.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "websockets", version = "15.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "uvicorn" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "click", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "h11", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/41/06cce5dbb9f77591512957710ac709e60b12e6216a2f2d0d607fd49706e8/uvicorn-0.50.0.tar.gz", hash = "sha256:0c92e1bc2259cb7faa4fcef774a5966588f2e88542744550b66799fba10b76f1", size = 93257, upload-time = "2026-07-04T05:03:26.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/3a/eb70620ca2bf8213603d5c731460687c49fee38b0072f0b4a637781f0a53/uvicorn-0.50.0-py3-none-any.whl", hash = "sha256:05f0eb19edf38208f79f43df8a63081b48df31b0cd1e5997be957a4dc97d1b19", size = 72716, upload-time = "2026-07-04T05:03:24.848Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "httptools", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "uvloop", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1b/6fbd611aeba01ef802c5876c94d7be603a9710db055beacbad39e75a31aa/uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4", size = 1345858, upload-time = "2025-10-16T22:17:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/9e/91/2c84f00bdbe3c51023cc83b027bac1fe959ba4a552e970da5ef0237f7945/uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c", size = 743913, upload-time = "2025-10-16T22:17:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/76aec83886d41a88aca5681db6a2c0601622d0d2cb66cd0d200587f962ad/uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54", size = 3635818, upload-time = "2025-10-16T22:17:13.812Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/733fcb815d345979fc54d3cdc3eb50bc75a47da3e4003ea7ada58e6daa65/uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659", size = 3685477, upload-time = "2025-10-16T22:17:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/83/fb/bee1eb11cc92bd91f76d97869bb6a816e80d59fd73721b0a3044dc703d9c/uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743", size = 3496128, upload-time = "2025-10-16T22:17:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/76/ee/3fdfeaa9776c0fd585d358c92b1dbca669720ffa476f0bbe64ed8f245bd7/uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7", size = 3602565, upload-time = "2025-10-16T22:17:17.755Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/a4/68/a7303a15cc797ab04d58f1fea7f67c50bd7f80090dfd7e750e7576e07582/watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", size = 409220, upload-time = "2025-10-14T15:05:51.917Z" }, + { url = "https://files.pythonhosted.org/packages/99/b8/d1857ce9ac76034c053fa7ef0e0ef92d8bd031e842ea6f5171725d31e88f/watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", size = 396712, upload-time = "2025-10-14T15:05:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/41/7a/da7ada566f48beaa6a30b13335b49d1f6febaf3a5ddbd1d92163a1002cf4/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", size = 451462, upload-time = "2025-10-14T15:05:54.742Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b2/7cb9e0d5445a8d45c4cccd68a590d9e3a453289366b96ff37d1075aaebef/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", size = 460811, upload-time = "2025-10-14T15:05:55.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/9d/b07d4491dde6db6ea6c680fdec452f4be363d65c82004faf2d853f59b76f/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", size = 490576, upload-time = "2025-10-14T15:05:56.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/03/e64dcab0a1806157db272a61b7891b062f441a30580a581ae72114259472/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", size = 597726, upload-time = "2025-10-14T15:05:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8e/a827cf4a8d5f2903a19a934dcf512082eb07675253e154d4cd9367978a58/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", size = 474900, upload-time = "2025-10-14T15:05:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/94fed0b346b85b22303a12eee5f431006fae6af70d841cac2f4403245533/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", size = 457521, upload-time = "2025-10-14T15:06:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/c4/64/bc3331150e8f3c778d48a4615d4b72b3d2d87868635e6c54bbd924946189/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", size = 632191, upload-time = "2025-10-14T15:06:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/84/f39e19549c2f3ec97225dcb2ceb9a7bb3c5004ed227aad1f321bf0ff2051/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", size = 623923, upload-time = "2025-10-14T15:06:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/0759ae15d9a0c9c5fe946bd4cf45ab9e7bad7cfede2c06dc10f59171b29f/watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", size = 274010, upload-time = "2025-10-14T15:06:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3b/eb26cddd4dfa081e2bf6918be3b2fc05ee3b55c1d21331d5562ee0c6aaad/watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", size = 289090, upload-time = "2025-10-14T15:06:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/38a2c52fdbbfe2fc7ffaaaaaebc927d52b9f4d5139bba3186c19a7463001/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", size = 409210, upload-time = "2025-10-14T15:06:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/d1/43/d7e8b71f6c21ff813ee8da1006f89b6c7fff047fb4c8b16ceb5e840599c5/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", size = 397286, upload-time = "2025-10-14T15:06:16.177Z" }, + { url = "https://files.pythonhosted.org/packages/1f/5d/884074a5269317e75bd0b915644b702b89de73e61a8a7446e2b225f45b1f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", size = 451768, upload-time = "2025-10-14T15:06:18.266Z" }, + { url = "https://files.pythonhosted.org/packages/17/71/7ffcaa9b5e8961a25026058058c62ec8f604d2a6e8e1e94bee8a09e1593f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", size = 458561, upload-time = "2025-10-14T15:06:19.323Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9", size = 400471, upload-time = "2026-05-18T04:31:08.908Z" }, + { url = "https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4", size = 394599, upload-time = "2026-05-18T04:30:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/18/52/752dcc7dc817baef5e89518732925795ce52e36a683a9a3c9fb68b21504e/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631", size = 455458, upload-time = "2026-05-18T04:30:29.126Z" }, + { url = "https://files.pythonhosted.org/packages/12/48/366ebbb22fcc504c2f72b45f0b7e72f40a18795cc01752c16066d597b67a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994", size = 460513, upload-time = "2026-05-18T04:31:40.85Z" }, + { url = "https://files.pythonhosted.org/packages/ad/44/1f9e1b15e7a729062e0d0c3d0d7225ea4ab98b2267ef87287153be2495fc/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e", size = 493616, upload-time = "2026-05-18T04:30:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/7e/55/8b1086dcc8a1d6a697a62767bd7ea368e74c61c6fd171683cfe24a3fe5d2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19", size = 573154, upload-time = "2026-05-18T04:30:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/14/7a/242f400cc77fafa7b18d53d19d9cb64fc6a6f61f28c55913bae7c674d92a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8", size = 467046, upload-time = "2026-05-18T04:30:41.869Z" }, + { url = "https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07", size = 457100, upload-time = "2026-05-18T04:31:13.064Z" }, + { url = "https://files.pythonhosted.org/packages/81/36/519f6dbb7a95e4fe7c1513ed25b1520295ef9905a27f1f2226a73892bfb7/watchfiles-1.2.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551", size = 467038, upload-time = "2026-05-18T04:30:32.915Z" }, + { url = "https://files.pythonhosted.org/packages/2f/12/951af6b9f89097e02511122258402cb3578443021930b70cf968d6310dc0/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310", size = 632563, upload-time = "2026-05-18T04:30:11.539Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/0cba1f0a6117b7ec117271bdc3cb3a5a252005959755a2c09a745e0942cc/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df", size = 660851, upload-time = "2026-05-18T04:31:53.186Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/26347558cc8bf6877845e66b315f644d03c173906aa09e233a3f4fd23928/watchfiles-1.2.0-cp310-cp310-win32.whl", hash = "sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1", size = 277023, upload-time = "2026-05-18T04:30:18.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/68/a5e67b6b68e94f4c1511d61c46c55eba0737583620b6febf194c7b9cc23f/watchfiles-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d", size = 290107, upload-time = "2026-05-18T04:32:09.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/36/db/3fff0bcbe339a6fa6a3b9e3fbc2bfb321ec2f4cd233692272c5a8d6cf801/websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5", size = 175424, upload-time = "2025-03-05T20:02:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/46/e6/519054c2f477def4165b0ec060ad664ed174e140b0d1cbb9fafa4a54f6db/websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a", size = 173077, upload-time = "2025-03-05T20:02:58.37Z" }, + { url = "https://files.pythonhosted.org/packages/1a/21/c0712e382df64c93a0d16449ecbf87b647163485ca1cc3f6cbadb36d2b03/websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b", size = 173324, upload-time = "2025-03-05T20:02:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cb/51ba82e59b3a664df54beed8ad95517c1b4dc1a913730e7a7db778f21291/websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770", size = 182094, upload-time = "2025-03-05T20:03:01.827Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0f/bf3788c03fec679bcdaef787518dbe60d12fe5615a544a6d4cf82f045193/websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb", size = 181094, upload-time = "2025-03-05T20:03:03.123Z" }, + { url = "https://files.pythonhosted.org/packages/5e/da/9fb8c21edbc719b66763a571afbaf206cb6d3736d28255a46fc2fe20f902/websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054", size = 181397, upload-time = "2025-03-05T20:03:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/2e/65/65f379525a2719e91d9d90c38fe8b8bc62bd3c702ac651b7278609b696c4/websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee", size = 181794, upload-time = "2025-03-05T20:03:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/31ac2d08f8e9304d81a1a7ed2851c0300f636019a57cbaa91342015c72cc/websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed", size = 181194, upload-time = "2025-03-05T20:03:08.844Z" }, + { url = "https://files.pythonhosted.org/packages/98/72/1090de20d6c91994cd4b357c3f75a4f25ee231b63e03adea89671cc12a3f/websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880", size = 181164, upload-time = "2025-03-05T20:03:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/2d/37/098f2e1c103ae8ed79b0e77f08d83b0ec0b241cf4b7f2f10edd0126472e1/websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411", size = 176381, upload-time = "2025-03-05T20:03:12.77Z" }, + { url = "https://files.pythonhosted.org/packages/75/8b/a32978a3ab42cebb2ebdd5b05df0696a09f4d436ce69def11893afa301f0/websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4", size = 176841, upload-time = "2025-03-05T20:03:14.367Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/48/4b67623bac4d79beb3a6bb27b803ba75c1bdedc06bd827e465803690a4b2/websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940", size = 173106, upload-time = "2025-03-05T20:03:29.404Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f0/adb07514a49fe5728192764e04295be78859e4a537ab8fcc518a3dbb3281/websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e", size = 173339, upload-time = "2025-03-05T20:03:30.755Z" }, + { url = "https://files.pythonhosted.org/packages/87/28/bd23c6344b18fb43df40d0700f6d3fffcd7cef14a6995b4f976978b52e62/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9", size = 174597, upload-time = "2025-03-05T20:03:32.247Z" }, + { url = "https://files.pythonhosted.org/packages/6d/79/ca288495863d0f23a60f546f0905ae8f3ed467ad87f8b6aceb65f4c013e4/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b", size = 174205, upload-time = "2025-03-05T20:03:33.731Z" }, + { url = "https://files.pythonhosted.org/packages/04/e4/120ff3180b0872b1fe6637f6f995bcb009fb5c87d597c1fc21456f50c848/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f", size = 174150, upload-time = "2025-03-05T20:03:35.757Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/30e2f9c539b8da8b1d76f64012f3b19253271a63413b2d3adb94b143407f/websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123", size = 176877, upload-time = "2025-03-05T20:03:37.199Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] From a5a3c8c91d58b8e2196c64cd211ab7e6bbf4446d Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 11 Jul 2026 16:44:02 +0200 Subject: [PATCH 21/94] feat(openai-agents): add OpenAI agents Python integration and example files --- apps/dojo/scripts/generate-content-json.ts | 15 ++++ apps/dojo/scripts/prep-dojo-everything.js | 5 ++ apps/dojo/scripts/run-dojo-everything.js | 14 +++ apps/dojo/src/agents.ts | 12 +++ apps/dojo/src/env.ts | 3 + apps/dojo/src/files.json | 92 ++++++++++++++++++++ apps/dojo/src/menu.ts | 10 +++ integrations/openai-agents/python/.gitignore | 1 - 8 files changed, 151 insertions(+), 1 deletion(-) diff --git a/apps/dojo/scripts/generate-content-json.ts b/apps/dojo/scripts/generate-content-json.ts index 31798cc3ef..a8399ed5ce 100644 --- a/apps/dojo/scripts/generate-content-json.ts +++ b/apps/dojo/scripts/generate-content-json.ts @@ -545,6 +545,21 @@ const agentFilesMapper: Record< {}, ); }, + "openai-agents-python": (agentKeys: string[]) => { + return agentKeys.reduce( + (acc, agentId) => ({ + ...acc, + [agentId]: [ + path.join( + __dirname, + integrationsFolderPath, + `/openai-agents/python/examples/agents_examples/${agentId}.py`, + ), + ], + }), + {}, + ); + }, // watsonx uses a single TS agent for all features — no per-feature server files watsonx: () => ({ agentic_chat: [ diff --git a/apps/dojo/scripts/prep-dojo-everything.js b/apps/dojo/scripts/prep-dojo-everything.js index 458895aef9..18fa863e5a 100755 --- a/apps/dojo/scripts/prep-dojo-everything.js +++ b/apps/dojo/scripts/prep-dojo-everything.js @@ -134,6 +134,11 @@ const ALL_TARGETS = { name: "Dojo (dev)", cwd: gitRoot, }, + "openai-agents-python": { + command: "uv sync", + name: "OpenAI Agents SDK (Python)", + cwd: path.join(integrationsRoot, "openai-agents/python/examples"), + }, "claude-agent-sdk-python": { command: "uv sync", name: "Claude Agent SDK (Python)", diff --git a/apps/dojo/scripts/run-dojo-everything.js b/apps/dojo/scripts/run-dojo-everything.js index be3fbedde1..898afb6aa7 100755 --- a/apps/dojo/scripts/run-dojo-everything.js +++ b/apps/dojo/scripts/run-dojo-everything.js @@ -219,6 +219,20 @@ const ALL_SERVICES = { env: { PORT: 8014 }, }, ], + "openai-agents-python": [ + { + command: "uv run dev", + name: "OpenAI Agents SDK (Python)", + cwd: path.join(integrationsRoot, "openai-agents/python/examples"), + env: { + PORT: 8022, + OPENAI_API_KEY: process.env.OPENAI_API_KEY || "test-key", + ...(!process.env.OPENAI_API_KEY && { + OPENAI_BASE_URL: "http://localhost:5555/v1", + }), + }, + }, + ], "claude-agent-sdk-python": [ { command: "uv run dev", diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 0d650c62b1..a13e0d2add 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -632,6 +632,18 @@ export const agentsIntegrations = { tool_based_generative_ui: "tool_based_generative_ui", }), + "openai-agents-python": async () => + mapAgents( + (path) => + new HttpAgent({ url: `${envVars.openaiAgentsPythonUrl}/${path}` }), + { + agentic_chat: "agentic_chat", + backend_tool_rendering: "backend_tool_rendering", + human_in_the_loop: "human_in_the_loop", + tool_based_generative_ui: "tool_based_generative_ui", + }, + ), + "claude-agent-sdk-python": async () => mapAgents( (path) => diff --git a/apps/dojo/src/env.ts b/apps/dojo/src/env.ts index 47d1cca5a9..4e78a8df35 100644 --- a/apps/dojo/src/env.ts +++ b/apps/dojo/src/env.ts @@ -25,6 +25,7 @@ type envVars = { awsStrandsTypescriptUrl: string; claudeAgentSdkPythonUrl: string; claudeAgentSdkTypescriptUrl: string; + openaiAgentsPythonUrl: string; langroidUrl: string; watsonxRegion: string; watsonxInstanceId: string; @@ -84,6 +85,8 @@ export default function getEnvVars(): envVars { process.env.CLAUDE_AGENT_SDK_PYTHON_URL || "http://localhost:8019", claudeAgentSdkTypescriptUrl: process.env.CLAUDE_AGENT_SDK_TYPESCRIPT_URL || "http://localhost:8020", + openaiAgentsPythonUrl: + process.env.OPENAI_AGENTS_PYTHON_URL || "http://localhost:8022", langroidUrl: process.env.LANGROID_URL || "http://localhost:8021", watsonxRegion: process.env.WATSONX_REGION || "", watsonxInstanceId: process.env.WATSONX_INSTANCE_ID || "", diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index c1d4c94695..804eb91e1c 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4813,6 +4813,98 @@ "type": "file" } ], + "openai-agents-python::agentic_chat": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useRenderTool,\n useAgentContext,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n\n useAgentContext({\n description: 'Name of the user',\n value: 'Bob'\n });\n\n useFrontendTool({\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients. Only use when asked.\"),\n }) ,\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n return {\n status: \"success\",\n message: `Background changed to ${background}`,\n };\n },\n });\n\n useRenderTool({\n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return
Loading weather...
;\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field reads as\n // undefined and the card renders empty values.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n return (\n
\n Weather in {parsed.city ?? args.location}\n
Temperature: {parsed.temperature}°C
\n
Humidity: {parsed.humidity}%
\n
Wind Speed: {parsed.windSpeed ?? parsed.wind_speed} mph
\n
Conditions: {parsed.conditions}
\n
\n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Change background\",\n message: \"Change the background to something new.\",\n },\n {\n title: \"Generate sonnet\",\n message: \"Write a short sonnet about AI.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n \n
\n \n
\n \n );\n};\n\nexport default AgenticChat;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "agentic_chat.py", + "content": "\"\"\"Agentic chat — plain conversation, no tools.\n\nThe baseline demo: exercises ``TEXT_MESSAGE_START/CONTENT/END`` only. Good\nfirst smoke test before trying the tool / handoff examples.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_agentic_chat_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n", + "language": "python", + "type": "file" + } + ], + "openai-agents-python::backend_tool_rendering": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { \n useRenderTool,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n useRenderTool({\n \n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return (\n
\n ⚙️ Retrieving weather...\n
\n );\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field falls\n // through to its `|| 0` default and the card shows 0° C.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n const weatherResult: WeatherToolResult = {\n temperature: parsed.temperature ?? 0,\n conditions: parsed.conditions ?? \"clear\",\n humidity: parsed.humidity ?? 0,\n windSpeed: parsed.wind_speed ?? parsed.windSpeed ?? 0,\n feelsLike:\n parsed.feels_like ?? parsed.feelsLike ?? parsed.temperature ?? 0,\n };\n\n const themeColor = getThemeColor(weatherResult.conditions);\n\n return (\n \n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Weather in San Francisco\",\n message: \"What's the weather like in San Francisco?\",\n },\n {\n title: \"Weather in New York\",\n message: \"Tell me about the weather in New York.\",\n },\n {\n title: \"Weather in Tokyo\",\n message: \"How's the weather in Tokyo today?\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\ninterface WeatherToolResult {\n temperature: number;\n conditions: string;\n humidity: number;\n windSpeed: number;\n feelsLike: number;\n}\n\nfunction getThemeColor(conditions: string): string {\n const conditionLower = conditions.toLowerCase();\n if (conditionLower.includes(\"clear\") || conditionLower.includes(\"sunny\")) {\n return \"#667eea\";\n }\n if (conditionLower.includes(\"rain\") || conditionLower.includes(\"storm\")) {\n return \"#4A5568\";\n }\n if (conditionLower.includes(\"cloud\")) {\n return \"#718096\";\n }\n if (conditionLower.includes(\"snow\")) {\n return \"#63B3ED\";\n }\n return \"#764ba2\";\n}\n\nfunction WeatherCard({\n location,\n themeColor,\n result,\n status,\n}: {\n location?: string;\n themeColor: string;\n result: WeatherToolResult;\n status: \"inProgress\" | \"executing\" | \"complete\";\n}) {\n return (\n \n
\n
\n
\n

\n {location}\n

\n

Current Weather

\n
\n \n
\n\n
\n
\n {result.temperature}° C\n \n {\" / \"}\n {((result.temperature * 9) / 5 + 32).toFixed(1)}° F\n \n
\n
{result.conditions}
\n
\n\n
\n
\n
\n

Humidity

\n

{result.humidity}%

\n
\n
\n

Wind

\n

{result.windSpeed} mph

\n
\n
\n

Feels Like

\n

{result.feelsLike}°

\n
\n
\n
\n
\n \n );\n}\n\nfunction WeatherIcon({ conditions }: { conditions: string }) {\n if (!conditions) return null;\n\n if (conditions.toLowerCase().includes(\"clear\") || conditions.toLowerCase().includes(\"sunny\")) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"rain\") ||\n conditions.toLowerCase().includes(\"drizzle\") ||\n conditions.toLowerCase().includes(\"snow\") ||\n conditions.toLowerCase().includes(\"thunderstorm\")\n ) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"fog\") ||\n conditions.toLowerCase().includes(\"cloud\") ||\n conditions.toLowerCase().includes(\"overcast\")\n ) {\n return ;\n }\n\n return ;\n}\n\n// Simple sun icon for the weather card\nfunction SunIcon() {\n return (\n \n \n \n \n );\n}\n\nfunction RainIcon() {\n return (\n \n {/* Cloud */}\n \n {/* Rain drops */}\n \n \n );\n}\n\nfunction CloudIcon() {\n return (\n \n \n \n );\n}\n\nexport default AgenticChat;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "style.css", + "content": ".copilotKitInput {\n border-bottom-left-radius: 0.75rem;\n border-bottom-right-radius: 0.75rem;\n border-top-left-radius: 0.75rem;\n border-top-right-radius: 0.75rem;\n border: 1px solid var(--copilot-kit-separator-color) !important;\n}\n\n.copilotKitChat {\n background-color: #fff !important;\n}\n", + "language": "css", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "backend_tool_rendering.py", + "content": "\"\"\"Backend tool rendering — a server-side ``@function_tool``.\n\nExercises ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` for a tool the\n*backend* owns and executes (as opposed to :mod:`human_in_the_loop`, where the\nfrontend owns execution). The SDK runs the tool body itself; the translator\njust reports the call and its result as they stream past.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, function_tool\n\nfrom .constants import DEFAULT_MODEL\n\n_WEATHER = {\n \"berlin\": \"Sunny, 24°C\",\n \"munich\": \"Partly cloudy, 22°C\",\n \"london\": \"Showers, 17°C\",\n \"paris\": \"Sunny, 26°C\",\n \"new york\": \"Clear, 19°C\",\n}\n\n\n@function_tool\ndef get_weather(city: str) -> str:\n \"\"\"Get the current weather for a city.\"\"\"\n weather = _WEATHER.get(city.lower())\n if weather is None:\n return f\"No weather data for {city!r}.\"\n return f\"The weather in {city.title()} is {weather}.\"\n\n\ndef create_backend_tool_agent() -> Agent:\n return Agent(\n name=\"weather_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a helpful weather assistant. Use the get_weather tool \"\n \"whenever the user asks about weather in a specific city.\"\n ),\n tools=[get_weather],\n )\n", + "language": "python", + "type": "file" + } + ], + "openai-agents-python::human_in_the_loop": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useHumanInTheLoop,\n useConfigureSuggestions,\n CopilotChat,\n CopilotChatConfigurationProvider,\n} from \"@copilotkit/react-core/v2\";\nimport { CopilotKit,\nuseLangGraphInterrupt } from \"@copilotkit/react-core\";\nimport { z } from \"zod\";\nimport { useTheme } from \"next-themes\";\n\ninterface HumanInTheLoopProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst HumanInTheLoop: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\ninterface Step {\n description: string;\n status: \"disabled\" | \"enabled\" | \"executing\";\n}\n\n// Shared UI Components\nconst StepContainer = ({ theme, children }: { theme?: string; children: React.ReactNode }) => (\n
\n \n {children}\n
\n \n);\n\nconst StepHeader = ({\n theme,\n enabledCount,\n totalCount,\n status,\n showStatus = false,\n}: {\n theme?: string;\n enabledCount: number;\n totalCount: number;\n status?: string;\n showStatus?: boolean;\n}) => (\n
\n
\n

\n Select Steps\n

\n
\n
\n {enabledCount}/{totalCount} Selected\n
\n {showStatus && (\n \n {status === \"executing\" ? \"Ready\" : \"Waiting\"}\n
\n )}\n
\n
\n\n \n 0 ? (enabledCount / totalCount) * 100 : 0}%` }}\n />\n \n \n);\n\nconst StepItem = ({\n step,\n theme,\n status,\n onToggle,\n disabled = false,\n}: {\n step: { description: string; status: string };\n theme?: string;\n status?: string;\n onToggle: () => void;\n disabled?: boolean;\n}) => (\n \n \n \n);\n\nconst ActionButton = ({\n variant,\n theme,\n disabled,\n onClick,\n children,\n}: {\n variant: \"primary\" | \"secondary\" | \"success\" | \"danger\";\n theme?: string;\n disabled?: boolean;\n onClick: () => void;\n children: React.ReactNode;\n}) => {\n const baseClasses = \"px-6 py-3 rounded-lg font-semibold transition-all duration-200\";\n const enabledClasses = \"hover:scale-105 shadow-md hover:shadow-lg\";\n const disabledClasses = \"opacity-50 cursor-not-allowed\";\n\n const variantClasses = {\n primary:\n \"bg-gradient-to-r from-purple-500 to-purple-700 hover:from-purple-600 hover:to-purple-800 text-white shadow-lg hover:shadow-xl\",\n secondary:\n theme === \"dark\"\n ? \"bg-slate-700 hover:bg-slate-600 text-white border border-slate-600 hover:border-slate-500\"\n : \"bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-300 hover:border-gray-400\",\n success:\n \"bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white shadow-lg hover:shadow-xl\",\n danger:\n \"bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white shadow-lg hover:shadow-xl\",\n };\n\n return (\n \n {children}\n \n );\n};\n\nconst DecorativeElements = ({\n theme,\n variant = \"default\",\n}: {\n theme?: string;\n variant?: \"default\" | \"success\" | \"danger\";\n}) => (\n <>\n \n \n \n);\nconst InterruptHumanInTheLoop: React.FC<{\n event: { value: { steps: Step[] } };\n resolve: (value: string) => void;\n}> = ({ event, resolve }) => {\n const { theme } = useTheme();\n\n // Parse and initialize steps data\n let initialSteps: Step[] = [];\n if (event.value && event.value.steps && Array.isArray(event.value.steps)) {\n initialSteps = event.value.steps.map((step: any) => ({\n description: typeof step === \"string\" ? step : step.description || \"\",\n status: typeof step === \"object\" && step.status ? step.status : \"enabled\",\n }));\n }\n\n const [localSteps, setLocalSteps] = useState(initialSteps);\n const enabledCount = localSteps.filter((step) => step.status === \"enabled\").length;\n\n const handleStepToggle = (index: number) => {\n setLocalSteps((prevSteps) =>\n prevSteps.map((step, i) =>\n i === index\n ? { ...step, status: step.status === \"enabled\" ? \"disabled\" : \"enabled\" }\n : step,\n ),\n );\n };\n\n const handlePerformSteps = () => {\n const selectedSteps = localSteps\n .filter((step) => step.status === \"enabled\")\n .map((step) => step.description);\n resolve(\"The user selected the following steps: \" + selectedSteps.join(\", \"));\n };\n\n return (\n \n \n\n
\n {localSteps.map((step, index) => (\n handleStepToggle(index)}\n />\n ))}\n
\n\n
\n \n \n Perform Steps\n \n {enabledCount}\n \n \n
\n\n \n
\n );\n};\n\nconst Chat = ({ integrationId }: { integrationId: string }) => {\n return (\n \n \n \n );\n};\n\nconst ChatContent = () => {\n useConfigureSuggestions({\n suggestions: [\n { title: \"Simple plan\", message: \"Please plan a trip to mars in 5 steps.\" },\n { title: \"Complex plan\", message: \"Please plan a pasta dish in 10 steps.\" },\n ],\n available: \"always\",\n });\n\n // Langgraph uses it's own hook to handle human-in-the-loop interactions via langgraph interrupts,\n // This hook won't do anything for other integrations.\n useLangGraphInterrupt({\n \n render: ({ event, resolve }) => ,\n });\n useHumanInTheLoop({\n agentId: \"human_in_the_loop\",\n name: \"generate_task_steps\",\n description: \"Generates a list of steps for the user to perform\",\n parameters: z.object({\n steps: z.array(\n z.object({\n description: z.string(),\n status: z.enum([\"enabled\", \"disabled\", \"executing\"]),\n }),\n ),\n }) ,\n // Note: In v1, `available` was used to disable this for langgraph integrations.\n // In v2, availability is handled at the agent/backend level.\n render: ({ args, respond, status }: any) => {\n return ;\n },\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nconst StepsFeedback = ({ args, respond, status }: { args: any; respond: any; status: any }) => {\n const { theme } = useTheme();\n const [localSteps, setLocalSteps] = useState([]);\n const [accepted, setAccepted] = useState(null);\n\n useEffect(() => {\n if (status === \"executing\" && localSteps.length === 0 && Array.isArray(args?.steps) && args.steps.length > 0) {\n setLocalSteps(args.steps);\n }\n }, [status, args?.steps, localSteps]);\n\n if (!Array.isArray(args?.steps) || args.steps.length === 0) {\n return <>;\n }\n\n const steps = Array.isArray(localSteps) && localSteps.length > 0 ? localSteps : args.steps;\n const enabledCount = steps.filter((step: any) => step.status === \"enabled\").length;\n\n const handleStepToggle = (index: number) => {\n setLocalSteps((prevSteps) =>\n prevSteps.map((step, i) =>\n i === index\n ? { ...step, status: step.status === \"enabled\" ? \"disabled\" : \"enabled\" }\n : step,\n ),\n );\n };\n\n const handleReject = () => {\n if (respond) {\n setAccepted(false);\n respond({ accepted: false });\n }\n };\n\n const handleConfirm = () => {\n if (respond) {\n const confirmedSteps = localSteps.filter((step) => step.status === \"enabled\");\n setAccepted(true);\n respond({ accepted: true, steps: confirmedSteps });\n }\n };\n\n return (\n \n \n\n
\n {steps.map((step: any, index: any) => (\n handleStepToggle(index)}\n disabled={status !== \"executing\"}\n />\n ))}\n
\n\n {/* Action Buttons - Different logic from InterruptHumanInTheLoop */}\n {accepted === null && (\n
\n \n \n Reject\n \n \n \n Confirm\n \n {enabledCount}\n \n \n
\n )}\n\n {/* Result State - Unique to StepsFeedback */}\n {accepted !== null && (\n
\n \n {accepted ? \"✓\" : \"✗\"}\n {accepted ? \"Accepted\" : \"Rejected\"}\n
\n \n )}\n\n \n
\n );\n};\n\nexport default HumanInTheLoop;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🤝 Human-in-the-Loop Task Planner\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **human-in-the-loop** capabilities:\n\n1. **Collaborative Planning**: The Copilot generates task steps and lets you\n decide which ones to perform\n2. **Interactive Decision Making**: Select or deselect steps to customize the\n execution plan\n3. **Adaptive Responses**: The Copilot adapts its execution based on your\n choices, even handling missing steps\n\n## How to Interact\n\nTry these steps to experience the demo:\n\n1. Ask your Copilot to help with a task, such as:\n\n - \"Make me a sandwich\"\n - \"Plan a weekend trip\"\n - \"Organize a birthday party\"\n - \"Start a garden\"\n\n2. Review the suggested steps provided by your Copilot\n\n3. Select or deselect steps using the checkboxes to customize the plan\n\n - Try removing essential steps to see how the Copilot adapts!\n\n4. Click \"Execute Plan\" to see the outcome based on your selections\n\n## ✨ Human-in-the-Loop Magic in Action\n\n**What's happening technically:**\n\n- The agent analyzes your request and breaks it down into logical steps\n- These steps are presented to you through a dynamic UI component\n- Your selections are captured as user input\n- The agent considers your choices when executing the plan\n- The agent adapts to missing steps with creative problem-solving\n\n**What you'll see in this demo:**\n\n- The Copilot provides a detailed, step-by-step plan for your task\n- You have complete control over which steps to include\n- If you remove essential steps, the Copilot provides entertaining and creative\n workarounds\n- The final execution reflects your choices, showing how human input shapes the\n outcome\n- Each response is tailored to your specific selections\n\nThis human-in-the-loop pattern creates a powerful collaborative experience where\nboth human judgment and AI capabilities work together to achieve better results\nthan either could alone!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "human_in_the_loop.py", + "content": "\"\"\"Human-in-the-loop — a *frontend*-owned tool, approved before execution.\n\nUnlike :mod:`backend_tool_rendering`, ``generate_task_steps`` has no server\nimplementation — it arrives per-request as an AG-UI client tool\n(``RunAgentInput.tools``), gets wrapped into an SDK ``FunctionTool`` proxy by\n``AGUIToSDKTranslator.translate_tools()``, and is merged onto this agent by\nthe run loop (see ``server.py``).\n\n``tool_use_behavior=StopAtTools(...)`` is the SDK's built-in for \"the model\nmay only *call* this tool, never execute it\": the run ends the moment the\nmodel emits the call, before the (dead-code) proxy body would ever run. The\nfrontend renders the steps for user approval, then sends the result back as\nan AG-UI ``ToolMessage`` in the *next* request — ordinary multi-turn history,\nno custom pause/resume machinery needed.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\n\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_task_steps\"\n\nINSTRUCTIONS = \"\"\"You are a task planning assistant that breaks work into clear, actionable steps.\n\nWhen the user asks for help with a task:\n1. Immediately call the `generate_task_steps` tool with an array of steps.\n Each step is an object: {\"description\": \"...\", \"status\": \"enabled\"}.\n2. Do not restate the steps as plain text — the frontend renders them.\n3. After the call, wait for the user to approve/select steps; do not call\n the tool again until they respond.\n\"\"\"\n\n\ndef create_human_in_the_loop_agent() -> Agent:\n return Agent(\n name=\"task_planner\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n", + "language": "python", + "type": "file" + } + ], + "openai-agents-python::tool_based_generative_ui": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useConfigureSuggestions,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport {\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselNext,\n CarouselPrevious,\n} from \"@/components/ui/carousel\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface ToolBasedGenerativeUIProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ninterface Haiku {\n japanese: string[];\n english: string[];\n image_name: string | null;\n gradient: string;\n}\n\nexport default function ToolBasedGenerativeUI({ params }: ToolBasedGenerativeUIProps) {\n const { integrationId } = React.use(params);\n const { chatDefaultOpen } = useURLParams();\n\n return (\n \n \n \n \n );\n}\n\nfunction SidebarWithSuggestions({ defaultOpen }: { defaultOpen: boolean }) {\n useConfigureSuggestions({\n suggestions: [\n { title: \"Nature Haiku\", message: \"Write me a haiku about nature.\" },\n { title: \"Ocean Haiku\", message: \"Create a haiku about the ocean.\" },\n { title: \"Spring Haiku\", message: \"Generate a haiku about spring.\" },\n ],\n available: \"always\",\n });\n\n return (\n \n );\n}\n\nconst VALID_IMAGE_NAMES = [\n \"Osaka_Castle_Turret_Stone_Wall_Pine_Trees_Daytime.jpg\",\n \"Tokyo_Skyline_Night_Tokyo_Tower_Mount_Fuji_View.jpg\",\n \"Itsukushima_Shrine_Miyajima_Floating_Torii_Gate_Sunset_Long_Exposure.jpg\",\n \"Takachiho_Gorge_Waterfall_River_Lush_Greenery_Japan.jpg\",\n \"Bonsai_Tree_Potted_Japanese_Art_Green_Foliage.jpeg\",\n \"Shirakawa-go_Gassho-zukuri_Thatched_Roof_Village_Aerial_View.jpg\",\n \"Ginkaku-ji_Silver_Pavilion_Kyoto_Japanese_Garden_Pond_Reflection.jpg\",\n \"Senso-ji_Temple_Asakusa_Cherry_Blossoms_Kimono_Umbrella.jpg\",\n \"Cherry_Blossoms_Sakura_Night_View_City_Lights_Japan.jpg\",\n \"Mount_Fuji_Lake_Reflection_Cherry_Blossoms_Sakura_Spring.jpg\",\n];\n\nfunction HaikuDisplay() {\n const [activeIndex, setActiveIndex] = useState(0);\n const [haikus, setHaikus] = useState([\n {\n japanese: [\"仮の句よ\", \"まっさらながら\", \"花を呼ぶ\"],\n english: [\"A placeholder verse—\", \"even in a blank canvas,\", \"it beckons flowers.\"],\n image_name: null,\n gradient: \"\",\n },\n ]);\n\n useFrontendTool(\n {\n agentId: \"tool_based_generative_ui\",\n name: \"generate_haiku\",\n parameters: z.object({\n japanese: z.array(z.string()).describe(\"3 lines of haiku in Japanese\"),\n english: z.array(z.string()).describe(\"3 lines of haiku translated to English\"),\n image_name: z.string().describe(`One relevant image name from: ${VALID_IMAGE_NAMES.join(\", \")}`),\n gradient: z.string().describe(\"CSS Gradient color for the background\"),\n }) ,\n followUp: false,\n handler: async ({ japanese, english, image_name, gradient }: { japanese: string[]; english: string[]; image_name: string; gradient: string }) => {\n const newHaiku: Haiku = {\n japanese: japanese || [],\n english: english || [],\n image_name: image_name || null,\n gradient: gradient || \"\",\n };\n setHaikus((prev) => [\n newHaiku,\n ...prev.filter((h) => h.english[0] !== \"A placeholder verse—\"),\n ]);\n setActiveIndex(0);\n return \"Haiku generated!\";\n },\n render: ({ args }: { args: Partial }) => {\n if (!args.japanese) return <>;\n return ;\n },\n },\n [haikus],\n );\n\n const currentHaiku = haikus[activeIndex];\n\n return (\n
\n
\n \n \n {haikus.map((haiku, index) => (\n \n \n \n ))}\n \n {haikus.length > 1 && (\n <>\n \n \n \n )}\n \n
\n
\n );\n}\n\nfunction HaikuCard({ haiku }: { haiku: Partial }) {\n return (\n \n {/* Decorative background elements */}\n
\n
\n\n {/* Haiku Text */}\n
\n {haiku.japanese?.map((line, index) => (\n \n \n {line}\n

\n \n {haiku.english?.[index]}\n

\n
\n ))}\n
\n\n {/* Image */}\n {haiku.image_name && (\n
\n
\n \n
\n
\n
\n )}\n
\n );\n}\n", + "language": "typescript", + "type": "file" + }, + { + "name": "style.css", + "content": ".page-background {\n /* Darker gradient background */\n background: linear-gradient(170deg, #e9ecef 0%, #ced4da 100%);\n}\n\n@keyframes fade-scale-in {\n from {\n opacity: 0;\n transform: translateY(10px) scale(0.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n/* Updated card entry animation */\n@keyframes pop-in {\n 0% {\n opacity: 0;\n transform: translateY(15px) scale(0.95);\n }\n 70% {\n opacity: 1;\n transform: translateY(-2px) scale(1.02);\n }\n 100% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n/* Animation for subtle background gradient movement */\n@keyframes animated-gradient {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n\n/* Animation for flash effect on apply */\n@keyframes flash-border-glow {\n 0% {\n /* Start slightly intensified */\n border-top-color: #ff5b4a !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 25px rgba(255, 91, 74, 0.5);\n }\n 50% {\n /* Peak intensity */\n border-top-color: #ff4733 !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 35px rgba(255, 71, 51, 0.7);\n }\n 100% {\n /* Return to default state appearance */\n border-top-color: #ff6f61 !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 10px rgba(255, 111, 97, 0.15);\n }\n}\n\n/* Existing animation for haiku lines */\n@keyframes fade-slide-in {\n from {\n opacity: 0;\n transform: translateX(-15px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n.animated-fade-in {\n /* Use the new pop-in animation */\n animation: pop-in 0.6s ease-out forwards;\n}\n\n.haiku-card {\n /* Subtle animated gradient background */\n background: linear-gradient(120deg, #ffffff 0%, #fdfdfd 50%, #ffffff 100%);\n background-size: 200% 200%;\n animation: animated-gradient 10s ease infinite;\n\n /* === Explicit Border Override Attempt === */\n /* 1. Set the default grey border for all sides */\n border: 1px solid #dee2e6;\n\n /* 2. Explicitly override the top border immediately after */\n border-top: 10px solid #ff6f61 !important; /* Orange top - Added !important */\n /* === End Explicit Border Override Attempt === */\n\n padding: 2.5rem 3rem;\n border-radius: 20px;\n\n /* Default glow intensity */\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 15px rgba(255, 111, 97, 0.25);\n text-align: left;\n max-width: 745px;\n margin: 3rem auto;\n min-width: 600px;\n\n /* Transition */\n transition: transform 0.35s ease, box-shadow 0.35s ease, border-top-width 0.35s ease, border-top-color 0.35s ease;\n}\n\n.haiku-card:hover {\n transform: translateY(-8px) scale(1.03);\n /* Enhanced shadow + Glow */\n box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 25px rgba(255, 91, 74, 0.5);\n /* Modify only top border properties */\n border-top-width: 14px !important; /* Added !important */\n border-top-color: #ff5b4a !important; /* Added !important */\n}\n\n.haiku-card .flex {\n margin-bottom: 1.5rem;\n}\n\n.haiku-card .flex.haiku-line { /* Target the lines specifically */\n margin-bottom: 1.5rem;\n opacity: 0; /* Start hidden for animation */\n animation: fade-slide-in 0.5s ease-out forwards;\n /* animation-delay is set inline in page.tsx */\n}\n\n/* Remove previous explicit color overrides - rely on Tailwind */\n/* .haiku-card p.text-4xl {\n color: #212529;\n}\n\n.haiku-card p.text-base {\n color: #495057;\n} */\n\n.haiku-card.applied-flash {\n /* Apply the flash animation once */\n /* Note: animation itself has !important on border-top-color */\n animation: flash-border-glow 0.6s ease-out forwards;\n}\n\n/* Styling for images within the main haiku card */\n.haiku-card-image {\n width: 9.5rem; /* Increased size (approx w-48) */\n height: 9.5rem; /* Increased size (approx h-48) */\n object-fit: cover;\n border-radius: 1.5rem; /* rounded-xl */\n border: 1px solid #e5e7eb;\n /* Enhanced shadow with subtle orange hint */\n box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1),\n 0 3px 6px rgba(0, 0, 0, 0.08),\n 0 0 10px rgba(255, 111, 97, 0.2);\n /* Inherit animation delay from inline style */\n animation-name: fadeIn;\n animation-duration: 0.5s;\n animation-fill-mode: both;\n}\n\n/* Styling for images within the suggestion card */\n.suggestion-card-image {\n width: 6.5rem; /* Increased slightly (w-20) */\n height: 6.5rem; /* Increased slightly (h-20) */\n object-fit: cover;\n border-radius: 1rem; /* Equivalent to rounded-md */\n border: 1px solid #d1d5db; /* Equivalent to border (using Tailwind gray-300) */\n margin-top: 0.5rem;\n /* Added shadow for suggestion images */\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1),\n 0 2px 4px rgba(0, 0, 0, 0.06);\n transition: all 0.2s ease-in-out; /* Added for smooth deselection */\n}\n\n/* Styling for the focused suggestion card image */\n.suggestion-card-image-focus {\n width: 6.5rem;\n height: 6.5rem;\n object-fit: cover;\n border-radius: 1rem;\n margin-top: 0.5rem;\n /* Highlight styles */\n border: 2px solid #ff6f61; /* Thicker, themed border */\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), /* Base shadow for depth */\n 0 0 12px rgba(255, 111, 97, 0.6); /* Orange glow */\n transform: scale(1.05); /* Slightly scale up */\n transition: all 0.2s ease-in-out; /* Smooth transition for focus */\n}\n\n/* Styling for the suggestion card container in the sidebar */\n.suggestion-card {\n border: 1px solid #dee2e6; /* Same default border as haiku-card */\n border-top: 10px solid #ff6f61; /* Same orange top border */\n border-radius: 0.375rem; /* Default rounded-md */\n /* Note: background-color is set by Tailwind bg-gray-100 */\n /* Other styles like padding, margin, flex are handled by Tailwind */\n}\n\n.suggestion-image-container {\n display: flex;\n gap: 1rem;\n justify-content: space-between;\n width: 100%;\n height: 6.5rem;\n}\n\n/* Mobile responsive styles - matches useMobileView hook breakpoint */\n@media (max-width: 767px) {\n .haiku-card {\n padding: 1rem 1.5rem; /* Reduced from 2.5rem 3rem */\n min-width: auto; /* Remove min-width constraint */\n max-width: 100%; /* Full width on mobile */\n margin: 1rem auto; /* Reduced margin */\n }\n\n .haiku-card-image {\n width: 5.625rem; /* 90px - smaller on mobile */\n height: 5.625rem; /* 90px - smaller on mobile */\n }\n\n .suggestion-card-image {\n width: 5rem; /* Slightly smaller on mobile */\n height: 5rem; /* Slightly smaller on mobile */\n }\n\n .suggestion-card-image-focus {\n width: 5rem; /* Slightly smaller on mobile */\n height: 5rem; /* Slightly smaller on mobile */\n }\n}\n", + "language": "css", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🪶 Tool-Based Generative UI Haiku Creator\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **tool-based generative UI** capabilities:\n\n1. **Frontend Rendering of Tool Calls**: Backend tool calls are automatically\n rendered in the UI\n2. **Dynamic UI Generation**: The UI updates in real-time as the agent generates\n content\n3. **Elegant Content Presentation**: Complex structured data (haikus) are\n beautifully displayed\n\n## How to Interact\n\nChat with your Copilot and ask for haikus about different topics:\n\n- \"Create a haiku about nature\"\n- \"Write a haiku about technology\"\n- \"Generate a haiku about the changing seasons\"\n- \"Make a humorous haiku about programming\"\n\nEach request will trigger the agent to generate a haiku and display it in a\nvisually appealing card format in the UI.\n\n## ✨ Tool-Based Generative UI in Action\n\n**What's happening technically:**\n\n- The agent processes your request and determines it should create a haiku\n- It calls a backend tool that returns structured haiku data\n- CopilotKit automatically renders this tool call in the frontend\n- The rendering is handled by the registered tool component in your React app\n- No manual state management is required to display the results\n\n**What you'll see in this demo:**\n\n- As you request a haiku, a beautifully formatted card appears in the UI\n- The haiku follows the traditional 5-7-5 syllable structure\n- Each haiku is presented with consistent styling\n- Multiple haikus can be generated in sequence\n- The UI adapts to display each new piece of content\n\nThis pattern of tool-based generative UI can be extended to create any kind of\ndynamic content - from data visualizations to interactive components, all driven\nby your Copilot's tool calls!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "tool_based_generative_ui.py", + "content": "\"\"\"Tool-based generative UI — frontend tool renders the content.\n\nLike :mod:`human_in_the_loop`, the tool (``generate_haiku``) is *client*-owned:\nit arrives per-request in ``RunAgentInput.tools``, gets wrapped into an SDK\n``FunctionTool`` proxy, and ``StopAtTools`` ends the run the moment the model\ncalls it. The difference is intent: here the tool call *is* the deliverable —\nthe frontend renders the haiku card from the streamed ``TOOL_CALL_ARGS``,\nno approval round-trip expected.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\n\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_haiku\"\n\nINSTRUCTIONS = \"\"\"You are a creative writing assistant that renders haikus with a UI component.\n\nWhen the user asks for a haiku:\n1. Immediately call the `generate_haiku` tool with the haiku data\n (Japanese lines, English lines, and any other fields the tool declares).\n2. Do NOT write the haiku as plain text — the frontend renders it.\n\nFor non-creative requests, respond normally without the tool.\n\"\"\"\n\n\ndef create_tool_based_generative_ui_agent() -> Agent:\n return Agent(\n name=\"haiku_assistant\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n", + "language": "python", + "type": "file" + } + ], "langroid::agentic_chat": [ { "name": "page.tsx", diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index f7cef402a2..4b494a3927 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -377,6 +377,16 @@ export const menuIntegrations = [ "tool_based_generative_ui", ], }, + { + id: "openai-agents-python", + name: "OpenAI Agents SDK (Python)", + features: [ + "agentic_chat", + "backend_tool_rendering", + "human_in_the_loop", + "tool_based_generative_ui", + ], + }, { id: "langroid", name: "Langroid", diff --git a/integrations/openai-agents/python/.gitignore b/integrations/openai-agents/python/.gitignore index 0807b9ba84..8ecaed6434 100644 --- a/integrations/openai-agents/python/.gitignore +++ b/integrations/openai-agents/python/.gitignore @@ -223,4 +223,3 @@ __marimo__/ # Dev .dev/ CLAUDE.md -examples/ \ No newline at end of file From af4b81ef0da02420005ae7d410869573af6651fb Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 12 Jul 2026 00:48:54 +0200 Subject: [PATCH 22/94] feat(openai-agents): update weather tool to return fixed data for all locations --- apps/dojo/src/files.json | 2 +- .../agents_examples/backend_tool_rendering.py | 17 +++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 804eb91e1c..1aff80293f 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4854,7 +4854,7 @@ }, { "name": "backend_tool_rendering.py", - "content": "\"\"\"Backend tool rendering — a server-side ``@function_tool``.\n\nExercises ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` for a tool the\n*backend* owns and executes (as opposed to :mod:`human_in_the_loop`, where the\nfrontend owns execution). The SDK runs the tool body itself; the translator\njust reports the call and its result as they stream past.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, function_tool\n\nfrom .constants import DEFAULT_MODEL\n\n_WEATHER = {\n \"berlin\": \"Sunny, 24°C\",\n \"munich\": \"Partly cloudy, 22°C\",\n \"london\": \"Showers, 17°C\",\n \"paris\": \"Sunny, 26°C\",\n \"new york\": \"Clear, 19°C\",\n}\n\n\n@function_tool\ndef get_weather(city: str) -> str:\n \"\"\"Get the current weather for a city.\"\"\"\n weather = _WEATHER.get(city.lower())\n if weather is None:\n return f\"No weather data for {city!r}.\"\n return f\"The weather in {city.title()} is {weather}.\"\n\n\ndef create_backend_tool_agent() -> Agent:\n return Agent(\n name=\"weather_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a helpful weather assistant. Use the get_weather tool \"\n \"whenever the user asks about weather in a specific city.\"\n ),\n tools=[get_weather],\n )\n", + "content": "\"\"\"Backend tool rendering — a server-side ``@function_tool``.\n\nExercises ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` for a tool the\n*backend* owns and executes (as opposed to :mod:`human_in_the_loop`, where the\nfrontend owns execution). The SDK runs the tool body itself; the translator\njust reports the call and its result as they stream past.\n\nThe dojo's weather card (apps/dojo .../backend_tool_rendering/page.tsx) reads\nthe tool result as a JSON object — temperature/conditions/humidity/\nwind_speed/feels_like — and the argument as `location`, matching every other\nintegration's version of this demo. A plain sentence string or a `city`\nparam renders as a blank/all-zero card, since the frontend has nothing to\nparse. The fixed return value (same for every location) matches how most\nother integrations' versions of this demo work too — the point of the demo\nis the tool-call plumbing, not real weather data.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, function_tool\n\nfrom .constants import DEFAULT_MODEL\n\n_WEATHER = {\"temperature\": 20, \"conditions\": \"sunny\", \"humidity\": 50, \"wind_speed\": 10, \"feels_like\": 20}\n\n\n@function_tool\ndef get_weather(location: str) -> dict:\n \"\"\"Get the current weather for a location.\"\"\"\n return _WEATHER\n\n\ndef create_backend_tool_agent() -> Agent:\n return Agent(\n name=\"weather_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a helpful weather assistant. Use the get_weather tool \"\n \"whenever the user asks about weather in a specific location.\"\n ),\n tools=[get_weather],\n )\n", "language": "python", "type": "file" } diff --git a/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py b/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py index 6743dbb1b0..0ad85abca9 100644 --- a/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py +++ b/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py @@ -10,7 +10,9 @@ wind_speed/feels_like — and the argument as `location`, matching every other integration's version of this demo. A plain sentence string or a `city` param renders as a blank/all-zero card, since the frontend has nothing to -parse. +parse. The fixed return value (same for every location) matches how most +other integrations' versions of this demo work too — the point of the demo +is the tool-call plumbing, not real weather data. """ from __future__ import annotations @@ -19,22 +21,13 @@ from .constants import DEFAULT_MODEL -_WEATHER = { - "berlin": {"temperature": 24, "conditions": "sunny", "humidity": 40, "wind_speed": 12, "feels_like": 25}, - "munich": {"temperature": 22, "conditions": "cloudy", "humidity": 55, "wind_speed": 8, "feels_like": 21}, - "london": {"temperature": 17, "conditions": "rainy", "humidity": 75, "wind_speed": 18, "feels_like": 16}, - "paris": {"temperature": 26, "conditions": "sunny", "humidity": 35, "wind_speed": 10, "feels_like": 27}, - "new york": {"temperature": 19, "conditions": "clear", "humidity": 45, "wind_speed": 14, "feels_like": 18}, - "san francisco": {"temperature": 16, "conditions": "cloudy", "humidity": 68, "wind_speed": 20, "feels_like": 15}, - "tokyo": {"temperature": 23, "conditions": "clear", "humidity": 60, "wind_speed": 9, "feels_like": 24}, -} -_DEFAULT_WEATHER = {"temperature": 20, "conditions": "clear", "humidity": 50, "wind_speed": 10, "feels_like": 20} +_WEATHER = {"temperature": 20, "conditions": "sunny", "humidity": 50, "wind_speed": 10, "feels_like": 20} @function_tool def get_weather(location: str) -> dict: """Get the current weather for a location.""" - return _WEATHER.get(location.lower(), _DEFAULT_WEATHER) + return _WEATHER def create_backend_tool_agent() -> Agent: From ba4cae8e41cb338f874005c51f07ce6b5e987fdb Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 12 Jul 2026 03:26:18 +0200 Subject: [PATCH 23/94] feat(openai-agents): add handoff and subagents demos to dojo --- apps/dojo/src/agents.ts | 2 + .../feature/(v2)/handoff/README.mdx | 26 ++ .../feature/(v2)/handoff/page.tsx | 51 ++++ .../feature/(v2)/subagents/README.mdx | 30 +++ .../feature/(v2)/subagents/page.tsx | 244 ++++++++++++++++++ apps/dojo/src/config.ts | 14 + apps/dojo/src/files.json | 40 +++ apps/dojo/src/menu.ts | 2 + apps/dojo/src/types/integration.ts | 2 + .../examples/agents_examples/__init__.py | 6 +- .../examples/agents_examples/handoff.py | 66 ++++- .../{orchestrator.py => subagents.py} | 13 +- 12 files changed, 476 insertions(+), 20 deletions(-) create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/README.mdx create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/page.tsx create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx rename integrations/openai-agents/python/examples/agents_examples/{orchestrator.py => subagents.py} (79%) diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index a13e0d2add..69d9c21ad3 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -641,6 +641,8 @@ export const agentsIntegrations = { backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", tool_based_generative_ui: "tool_based_generative_ui", + handoff: "handoff", + subagents: "subagents", }, ), diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/README.mdx new file mode 100644 index 0000000000..412abf37bb --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/README.mdx @@ -0,0 +1,26 @@ +# 🤝 Handoff + +## What This Demo Shows + +Two handoff patterns from the SDK docs' `handoffs/` page, combined in one +triage agent: + +- passing an `Agent` directly (`billing_agent`) — plain handoff, no + customization +- wrapping with `handoff(agent=..., on_handoff=..., input_type=...)` + (`escalation_agent`) — the LLM fills in a structured `EscalationData` + reason, delivered to a callback before the specialist takes over + +## How to Interact + +- "I have a question about my invoice" (routes to billing_agent) +- "This isn't working and I've tried everything, I need to talk to someone" + (routes to escalation_agent with a captured reason) + +## Technical Details + +- The handoff shows up as an AG-UI tool call + result, same as any other tool +- `escalation_agent`'s structured input rides through as ordinary tool-call + JSON args — no special-casing in the translator +- Each hop emits `STEP_FINISHED` for the outgoing agent and `STEP_STARTED` + for the incoming one diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/page.tsx new file mode 100644 index 0000000000..c15b245c12 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/page.tsx @@ -0,0 +1,51 @@ +"use client"; +import React from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { CopilotChat, useConfigureSuggestions } from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; + +interface HandoffProps { + params: Promise<{ + integrationId: string; + }>; +} + +const Handoff: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + ); +}; + +const Chat = () => { + useConfigureSuggestions({ + suggestions: [ + { + title: "Billing question", + message: "I have a question about my last invoice", + }, + { + title: "Escalate an issue", + message: "This isn't working and I've tried everything, I need to escalate this", + }, + ], + available: "always", + }); + + return ( +
+
+ +
+
+ ); +}; + +export default Handoff; diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx new file mode 100644 index 0000000000..5917690701 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx @@ -0,0 +1,30 @@ +# 🧭 Subagents + +## What This Demo Shows + +A supervisor agent stays in charge of the conversation and calls specialist +agents as tools (`Agent.as_tool()`), possibly several in one turn, then +synthesizes their outputs itself — a call-and-return delegation, not a +handoff. Complements the `handoff` demo, where control transfers away from +the triage agent instead. Same shape as CopilotKit's own LangGraph +"subagents" showcase demo (a supervisor calling child agents as tools). + +A sidebar tracks the team live: role chips light up while a specialist is +working, and a delegation log lists each call with its status and a +preview of the input/output. + +## How to Interact + +- "Write a short piece about the history of coffee" (calls research, + writer, and critic specialists in sequence, then produces a final draft) +- "What is 2 + 2?" (answered directly, no team involved) + +## Technical Details + +- Each specialist invocation is an ordinary `TOOL_CALL_START/ARGS/END` + + `TOOL_CALL_RESULT` sequence +- The nested agents' own model turns stay internal to the SDK and are not + streamed as separate messages — the client sees one coherent transcript +- The sidebar is built from three `useRenderTool` renderers (one per + specialist tool name), each reporting its status into shared page state + via a small tracker component diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx new file mode 100644 index 0000000000..4ea57240b1 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx @@ -0,0 +1,244 @@ +"use client"; +import React, { useCallback, useEffect, useState } from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { CopilotChat, useConfigureSuggestions, useRenderTool } from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; +import { Badge } from "@/components/ui/badge"; +import { z } from "zod"; + +interface SubagentsProps { + params: Promise<{ + integrationId: string; + }>; +} + +type Role = "research" | "writer" | "critic"; +type Status = "inProgress" | "executing" | "complete"; + +const ROLES: { role: Role; label: string; emoji: string }[] = [ + { role: "research", label: "Researcher", emoji: "🔎" }, + { role: "writer", label: "Writer", emoji: "✍️" }, + { role: "critic", label: "Critic", emoji: "🧐" }, +]; + +interface DelegationEntry { + id: string; + role: Role; + status: Status; + text: string; +} + +const Subagents: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + ); +}; + +const SubagentsView = () => { + const [active, setActive] = useState>({ + research: null, + writer: null, + critic: null, + }); + const [log, setLog] = useState([]); + + // Called by each DelegationTracker instance as its tool call's status + // changes. Same shared state feeds both the role chips (who's working + // right now) and the log (what happened, in order). + const track = useCallback((entry: DelegationEntry) => { + setActive((prev) => ({ + ...prev, + [entry.role]: entry.status === "complete" ? null : entry.status, + })); + setLog((prev) => { + const idx = prev.findIndex((e) => e.id === entry.id); + if (idx === -1) return [...prev, entry]; + const next = [...prev]; + next[idx] = entry; + return next; + }); + }, []); + + useConfigureSuggestions({ + suggestions: [ + { + title: "History of AI", + message: "Write a short article about the history of AI", + }, + { + title: "History of programming languages", + message: "Write a short piece about the history of programming languages", + }, + ], + available: "always", + }); + + useRenderTool({ + name: "research_topic", + agentId: "subagents", + parameters: z.object({ topic: z.string().optional() }), + render: ({ toolCallId, status, args, result }: any) => ( + + ), + }); + + useRenderTool({ + name: "write_prose", + agentId: "subagents", + parameters: z.object({ facts: z.string().optional() }), + render: ({ toolCallId, status, args, result }: any) => ( + + ), + }); + + useRenderTool({ + name: "critique_draft", + agentId: "subagents", + parameters: z.object({ draft: z.string().optional() }), + render: ({ toolCallId, status, args, result }: any) => ( + + ), + }); + + return ( +
+
+

Supervisor's team

+
+ {ROLES.map(({ role, label, emoji }) => { + const status = active[role]; + return ( +
+ {emoji} + {label} + {status && ( + + {status === "executing" ? "working" : "starting"} + + )} +
+ ); + })} +
+ +

+ Delegation log +

+
+ {log.length === 0 && ( +
No delegations yet.
+ )} + {log.map((entry) => { + const roleInfo = ROLES.find((r) => r.role === entry.role); + return ( +
+
+ + {roleInfo?.emoji} {roleInfo?.label} + + + {entry.status} + +
+ {entry.text && ( +
+ {entry.text} +
+ )} +
+ ); + })} +
+
+ +
+
+ +
+
+
+ ); +}; + +// Each tool call renders its own instance of this. `render` is invoked as a +// real component (not a plain function), so effects are legal here — this +// is what reports status upstream into the shared chip/log state on every +// inProgress → executing → complete transition, in addition to rendering +// its own inline card in the chat transcript. +function DelegationTracker({ + role, + toolCallId, + status, + preview, + result, + onUpdate, +}: { + role: Role; + toolCallId: string; + status: Status; + preview?: string; + result?: string; + onUpdate: (entry: DelegationEntry) => void; +}) { + const text = status === "complete" ? (result ?? "") : (preview ?? ""); + + useEffect(() => { + onUpdate({ id: toolCallId, role, status, text }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [toolCallId, role, status, text]); + + const roleInfo = ROLES.find((r) => r.role === role)!; + const label = + status === "complete" + ? `${roleInfo.label} finished` + : status === "executing" + ? `${roleInfo.label} working…` + : `Calling ${roleInfo.label.toLowerCase()}…`; + + return ( +
+ {roleInfo.emoji} {label} +
+ ); +} + +export default Subagents; diff --git a/apps/dojo/src/config.ts b/apps/dojo/src/config.ts index 305bfa85dd..2508af2d90 100644 --- a/apps/dojo/src/config.ts +++ b/apps/dojo/src/config.ts @@ -88,6 +88,20 @@ export const featureConfig: FeatureConfig[] = [ "Have your tasks performed by multiple agents, working together", tags: ["Chat", "Multi-agent architecture", "Streaming", "Subgraphs"], }), + createFeatureConfig({ + id: "handoff", + name: "Handoff", + description: + "Triage agent hands the conversation off to a specialist agent", + tags: ["Multi-agent architecture", "Agentic architecture", "Handoff", "Delegation"], + }), + createFeatureConfig({ + id: "subagents", + name: "Sub-agents", + description: + "Supervisor agent calls specialist agents as tools and synthesizes the result", + tags: ["Multi-agent architecture", "Sub-agents", "Orchestrator", "Agent as tool"], + }), createFeatureConfig({ id: "a2a_chat", name: "A2A Chat", diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 1aff80293f..503ecb1f99 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4905,6 +4905,46 @@ "type": "file" } ], + "openai-agents-python::handoff": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useConfigureSuggestions } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface HandoffProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst Handoff: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Billing question\",\n message: \"I have a question about my last invoice\",\n },\n {\n title: \"Escalate an issue\",\n message: \"This isn't working and I've tried everything, I need to escalate this\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default Handoff;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🤝 Handoff\n\n## What This Demo Shows\n\nTwo handoff patterns from the SDK docs' `handoffs/` page, combined in one\ntriage agent:\n\n- passing an `Agent` directly (`billing_agent`) — plain handoff, no\n customization\n- wrapping with `handoff(agent=..., on_handoff=..., input_type=...)`\n (`escalation_agent`) — the LLM fills in a structured `EscalationData`\n reason, delivered to a callback before the specialist takes over\n\n## How to Interact\n\n- \"I have a question about my invoice\" (routes to billing_agent)\n- \"This isn't working and I've tried everything, I need to talk to someone\"\n (routes to escalation_agent with a captured reason)\n\n## Technical Details\n\n- The handoff shows up as an AG-UI tool call + result, same as any other tool\n- `escalation_agent`'s structured input rides through as ordinary tool-call\n JSON args — no special-casing in the translator\n- Each hop emits `STEP_FINISHED` for the outgoing agent and `STEP_STARTED`\n for the incoming one\n", + "language": "markdown", + "type": "file" + }, + { + "name": "handoff.py", + "content": "\"\"\"Handoff — the SDK docs' own patterns from ``handoffs/``, wired for AG-UI.\n\nCombines the two handoff shapes shown on that page in one triage agent:\n\n* ``handoffs=[billing_agent]`` — passing an ``Agent`` directly (the\n \"Basic usage\" section). No customization; the SDK derives the\n ``transfer_to_billing_agent`` tool itself.\n* ``handoffs=[handoff(agent=..., on_handoff=..., input_type=...)]`` — the\n \"Handoff inputs\" section: a Pydantic model the LLM fills in when it\n triggers the handoff, delivered to ``on_handoff`` before the specialist\n ever sees the conversation. Used here for escalation_agent so the reason\n is captured structurally instead of buried in free text.\n\nAlso uses ``RECOMMENDED_PROMPT_PREFIX`` (the \"Recommended prompts\" section)\non every agent that receives handoffs, since the docs call out that models\nhandle multi-agent handoffs more reliably with it in the instructions.\n\nExercises:\n\n* ``translate_handoff_call_item`` / ``translate_handoff_output_item`` — the\n handoff shows up as an AG-UI tool call + result, same as any other tool.\n ``escalation_agent``'s structured input rides through as ordinary\n ``TOOL_CALL_ARGS`` JSON — no special-casing needed, since the translator\n dispatches on run-item type, not on whether the tool happens to be a\n handoff.\n* ``translate_agent_updated_event`` — each hop emits ``STEP_FINISHED`` for\n the outgoing agent and ``STEP_STARTED`` for the incoming one, so the\n client can label which agent produced which message.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, RunContextWrapper, handoff\nfrom agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX\nfrom pydantic import BaseModel\n\nfrom .constants import DEFAULT_MODEL\n\nbilling_agent = Agent(\n name=\"billing_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n f\"{RECOMMENDED_PROMPT_PREFIX}\\n\"\n \"You handle billing questions: invoices, charges, payment methods. \"\n \"Be precise and concise.\"\n ),\n)\n\nescalation_agent = Agent(\n name=\"escalation_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n f\"{RECOMMENDED_PROMPT_PREFIX}\\n\"\n \"You handle escalated issues that the triage agent couldn't resolve. \"\n \"Acknowledge the reason you were given and ask what outcome the \"\n \"customer is looking for.\"\n ),\n)\n\n\nclass EscalationData(BaseModel):\n \"\"\"Structured input the triage agent fills in when escalating.\"\"\"\n\n reason: str\n\n\nasync def on_escalation_handoff(ctx: RunContextWrapper[None], input_data: EscalationData) -> None:\n print(f\"Escalation agent called with reason: {input_data.reason}\")\n\n\ndef create_handoff_agent() -> Agent:\n return Agent(\n name=\"triage_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n f\"{RECOMMENDED_PROMPT_PREFIX}\\n\"\n \"You triage customer support requests. Hand off to billing_agent \"\n \"for billing questions. For anything you can't resolve yourself, \"\n \"hand off to escalation_agent with a short reason. Handle \"\n \"anything else yourself.\"\n ),\n handoffs=[\n billing_agent,\n handoff(\n agent=escalation_agent,\n on_handoff=on_escalation_handoff,\n input_type=EscalationData,\n ),\n ],\n )\n", + "language": "python", + "type": "file" + } + ], + "openai-agents-python::subagents": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useConfigureSuggestions, useRenderTool } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { z } from \"zod\";\n\ninterface SubagentsProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ntype Role = \"research\" | \"writer\" | \"critic\";\ntype Status = \"inProgress\" | \"executing\" | \"complete\";\n\nconst ROLES: { role: Role; label: string; emoji: string }[] = [\n { role: \"research\", label: \"Researcher\", emoji: \"🔎\" },\n { role: \"writer\", label: \"Writer\", emoji: \"✍️\" },\n { role: \"critic\", label: \"Critic\", emoji: \"🧐\" },\n];\n\ninterface DelegationEntry {\n id: string;\n role: Role;\n status: Status;\n text: string;\n}\n\nconst Subagents: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst SubagentsView = () => {\n const [active, setActive] = useState>({\n research: null,\n writer: null,\n critic: null,\n });\n const [log, setLog] = useState([]);\n\n // Called by each DelegationTracker instance as its tool call's status\n // changes. Same shared state feeds both the role chips (who's working\n // right now) and the log (what happened, in order).\n const track = useCallback((entry: DelegationEntry) => {\n setActive((prev) => ({\n ...prev,\n [entry.role]: entry.status === \"complete\" ? null : entry.status,\n }));\n setLog((prev) => {\n const idx = prev.findIndex((e) => e.id === entry.id);\n if (idx === -1) return [...prev, entry];\n const next = [...prev];\n next[idx] = entry;\n return next;\n });\n }, []);\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"History of AI\",\n message: \"Write a short article about the history of AI\",\n },\n {\n title: \"History of programming languages\",\n message: \"Write a short piece about the history of programming languages\",\n },\n ],\n available: \"always\",\n });\n\n useRenderTool({\n name: \"research_topic\",\n agentId: \"subagents\",\n parameters: z.object({ topic: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"write_prose\",\n agentId: \"subagents\",\n parameters: z.object({ facts: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"critique_draft\",\n agentId: \"subagents\",\n parameters: z.object({ draft: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n return (\n
\n
\n

Supervisor's team

\n
\n {ROLES.map(({ role, label, emoji }) => {\n const status = active[role];\n return (\n \n {emoji}\n {label}\n {status && (\n \n {status === \"executing\" ? \"working\" : \"starting\"}\n \n )}\n
\n );\n })}\n
\n\n

\n Delegation log\n

\n
\n {log.length === 0 && (\n
No delegations yet.
\n )}\n {log.map((entry) => {\n const roleInfo = ROLES.find((r) => r.role === entry.role);\n return (\n \n
\n \n {roleInfo?.emoji} {roleInfo?.label}\n \n \n {entry.status}\n \n
\n {entry.text && (\n
\n {entry.text}\n
\n )}\n
\n );\n })}\n
\n
\n\n
\n
\n \n
\n
\n \n );\n};\n\n// Each tool call renders its own instance of this. `render` is invoked as a\n// real component (not a plain function), so effects are legal here — this\n// is what reports status upstream into the shared chip/log state on every\n// inProgress → executing → complete transition, in addition to rendering\n// its own inline card in the chat transcript.\nfunction DelegationTracker({\n role,\n toolCallId,\n status,\n preview,\n result,\n onUpdate,\n}: {\n role: Role;\n toolCallId: string;\n status: Status;\n preview?: string;\n result?: string;\n onUpdate: (entry: DelegationEntry) => void;\n}) {\n const text = status === \"complete\" ? (result ?? \"\") : (preview ?? \"\");\n\n useEffect(() => {\n onUpdate({ id: toolCallId, role, status, text });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [toolCallId, role, status, text]);\n\n const roleInfo = ROLES.find((r) => r.role === role)!;\n const label =\n status === \"complete\"\n ? `${roleInfo.label} finished`\n : status === \"executing\"\n ? `${roleInfo.label} working…`\n : `Calling ${roleInfo.label.toLowerCase()}…`;\n\n return (\n
\n {roleInfo.emoji} {label}\n
\n );\n}\n\nexport default Subagents;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🧭 Subagents\n\n## What This Demo Shows\n\nA supervisor agent stays in charge of the conversation and calls specialist\nagents as tools (`Agent.as_tool()`), possibly several in one turn, then\nsynthesizes their outputs itself — a call-and-return delegation, not a\nhandoff. Complements the `handoff` demo, where control transfers away from\nthe triage agent instead. Same shape as CopilotKit's own LangGraph\n\"subagents\" showcase demo (a supervisor calling child agents as tools).\n\nA sidebar tracks the team live: role chips light up while a specialist is\nworking, and a delegation log lists each call with its status and a\npreview of the input/output.\n\n## How to Interact\n\n- \"Write a short piece about the history of coffee\" (calls research,\n writer, and critic specialists in sequence, then produces a final draft)\n- \"What is 2 + 2?\" (answered directly, no team involved)\n\n## Technical Details\n\n- Each specialist invocation is an ordinary `TOOL_CALL_START/ARGS/END` +\n `TOOL_CALL_RESULT` sequence\n- The nested agents' own model turns stay internal to the SDK and are not\n streamed as separate messages — the client sees one coherent transcript\n- The sidebar is built from three `useRenderTool` renderers (one per\n specialist tool name), each reporting its status into shared page state\n via a small tracker component\n", + "language": "markdown", + "type": "file" + }, + { + "name": "subagents.py", + "content": "\"\"\"Subagents — multi-agent via the SDK's agents-as-tools pattern.\n\nComplements :mod:`handoff`: there, control *transfers* to the specialist\n(triage agent exits the conversation). Here the supervisor stays in charge\nand *calls* specialists as tools (``Agent.as_tool()``), possibly several in\none turn, then synthesizes their outputs itself — a call-and-return\ndelegation, not a handoff. Same shape as the LangGraph \"subagents\" showcase\ndemo (a supervisor calling child agents as tools and getting results back),\njust built with the SDK's own ``as_tool()`` instead of a routing graph.\n\nOn the AG-UI side each specialist invocation is an ordinary\n``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested\nagent's own model turns stay internal to the SDK and are not streamed as\nseparate messages, so the client sees one coherent supervisor transcript.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\nresearch_agent = Agent(\n name=\"research_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You research a topic and return the key facts as a short bullet \"\n \"list. Facts only — no fluff, no conclusions.\"\n ),\n)\n\nwriter_agent = Agent(\n name=\"writer_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You turn bullet-point facts into short, engaging prose. One tight \"\n \"paragraph unless asked otherwise.\"\n ),\n)\n\ncritic_agent = Agent(\n name=\"critic_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You review a draft and return concrete improvement suggestions as a \"\n \"numbered list. Max 3 suggestions, be specific.\"\n ),\n)\n\n\ndef create_subagents_agent() -> Agent:\n return Agent(\n name=\"supervisor\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You supervise a small content team. For writing requests: \"\n \"call research_topic for the facts, then write_prose to draft, \"\n \"then critique_draft to review, then produce the final version \"\n \"yourself incorporating the critique. For simple questions, \"\n \"answer directly without the team.\"\n ),\n tools=[\n research_agent.as_tool(\n tool_name=\"research_topic\",\n tool_description=\"Research a topic and return key facts as bullets.\",\n ),\n writer_agent.as_tool(\n tool_name=\"write_prose\",\n tool_description=\"Turn bullet-point facts into short prose.\",\n ),\n critic_agent.as_tool(\n tool_name=\"critique_draft\",\n tool_description=\"Review a draft and suggest up to 3 improvements.\",\n ),\n ],\n )\n", + "language": "python", + "type": "file" + } + ], "langroid::agentic_chat": [ { "name": "page.tsx", diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index 4b494a3927..94e1b54e18 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -385,6 +385,8 @@ export const menuIntegrations = [ "backend_tool_rendering", "human_in_the_loop", "tool_based_generative_ui", + "handoff", + "subagents", ], }, { diff --git a/apps/dojo/src/types/integration.ts b/apps/dojo/src/types/integration.ts index 6be810e9d6..b255309134 100644 --- a/apps/dojo/src/types/integration.ts +++ b/apps/dojo/src/types/integration.ts @@ -20,6 +20,8 @@ export type Feature = | "a2ui_advanced" | "a2ui_recovery" | "crew_chat" + | "handoff" + | "subagents" | "error_flow" | "background_agents" | "observational_memory"; diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py index 56e7a85362..10006482a0 100644 --- a/integrations/openai-agents/python/examples/agents_examples/__init__.py +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -18,7 +18,7 @@ from .backend_tool_rendering import create_backend_tool_agent from .handoff import create_handoff_agent from .human_in_the_loop import create_human_in_the_loop_agent -from .orchestrator import create_orchestrator_agent +from .subagents import create_subagents_agent from .tool_based_generative_ui import create_tool_based_generative_ui_agent @@ -37,7 +37,7 @@ def build_registry() -> dict[str, DemoConfig]: "human_in_the_loop": DemoConfig(agent=create_human_in_the_loop_agent()), "tool_based_generative_ui": DemoConfig(agent=create_tool_based_generative_ui_agent()), "handoff": DemoConfig(agent=create_handoff_agent()), - "orchestrator": DemoConfig(agent=create_orchestrator_agent()), + "subagents": DemoConfig(agent=create_subagents_agent()), } @@ -48,6 +48,6 @@ def build_registry() -> dict[str, DemoConfig]: "create_backend_tool_agent", "create_handoff_agent", "create_human_in_the_loop_agent", - "create_orchestrator_agent", + "create_subagents_agent", "create_tool_based_generative_ui_agent", ] diff --git a/integrations/openai-agents/python/examples/agents_examples/handoff.py b/integrations/openai-agents/python/examples/agents_examples/handoff.py index c2f8e88f25..84bf4d2182 100644 --- a/integrations/openai-agents/python/examples/agents_examples/handoff.py +++ b/integrations/openai-agents/python/examples/agents_examples/handoff.py @@ -1,10 +1,28 @@ -"""Handoff — multi-agent triage using the SDK's native ``handoffs=``. +"""Handoff — the SDK docs' own patterns from ``handoffs/``, wired for AG-UI. -The triage agent hands the conversation to a specialist via the SDK's -built-in handoff mechanism (no custom routing code). Exercises: +Combines the two handoff shapes shown on that page in one triage agent: + +* ``handoffs=[billing_agent]`` — passing an ``Agent`` directly (the + "Basic usage" section). No customization; the SDK derives the + ``transfer_to_billing_agent`` tool itself. +* ``handoffs=[handoff(agent=..., on_handoff=..., input_type=...)]`` — the + "Handoff inputs" section: a Pydantic model the LLM fills in when it + triggers the handoff, delivered to ``on_handoff`` before the specialist + ever sees the conversation. Used here for escalation_agent so the reason + is captured structurally instead of buried in free text. + +Also uses ``RECOMMENDED_PROMPT_PREFIX`` (the "Recommended prompts" section) +on every agent that receives handoffs, since the docs call out that models +handle multi-agent handoffs more reliably with it in the instructions. + +Exercises: * ``translate_handoff_call_item`` / ``translate_handoff_output_item`` — the - handoff shows up as a AG-UI tool call + result, same as any other tool. + handoff shows up as an AG-UI tool call + result, same as any other tool. + ``escalation_agent``'s structured input rides through as ordinary + ``TOOL_CALL_ARGS`` JSON — no special-casing needed, since the translator + dispatches on run-item type, not on whether the tool happens to be a + handoff. * ``translate_agent_updated_event`` — each hop emits ``STEP_FINISHED`` for the outgoing agent and ``STEP_STARTED`` for the incoming one, so the client can label which agent produced which message. @@ -12,7 +30,9 @@ from __future__ import annotations -from agents import Agent +from agents import Agent, RunContextWrapper, handoff +from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX +from pydantic import BaseModel from .constants import DEFAULT_MODEL @@ -20,29 +40,51 @@ name="billing_agent", model=DEFAULT_MODEL, instructions=( + f"{RECOMMENDED_PROMPT_PREFIX}\n" "You handle billing questions: invoices, charges, payment methods. " "Be precise and concise." ), ) -refund_agent = Agent( - name="refund_agent", +escalation_agent = Agent( + name="escalation_agent", model=DEFAULT_MODEL, instructions=( - "You handle refund requests. Ask for the order id if missing, then " - "confirm the refund amount and timeline." + f"{RECOMMENDED_PROMPT_PREFIX}\n" + "You handle escalated issues that the triage agent couldn't resolve. " + "Acknowledge the reason you were given and ask what outcome the " + "customer is looking for." ), ) +class EscalationData(BaseModel): + """Structured input the triage agent fills in when escalating.""" + + reason: str + + +async def on_escalation_handoff(ctx: RunContextWrapper[None], input_data: EscalationData) -> None: + print(f"Escalation agent called with reason: {input_data.reason}") + + def create_handoff_agent() -> Agent: return Agent( name="triage_agent", model=DEFAULT_MODEL, instructions=( + f"{RECOMMENDED_PROMPT_PREFIX}\n" "You triage customer support requests. Hand off to billing_agent " - "for billing questions, or refund_agent for refund requests. " - "Handle anything else yourself." + "for billing questions. For anything you can't resolve yourself, " + "hand off to escalation_agent with a short reason. Handle " + "anything else yourself." ), - handoffs=[billing_agent, refund_agent], + handoffs=[ + billing_agent, + handoff( + agent=escalation_agent, + on_handoff=on_escalation_handoff, + input_type=EscalationData, + ), + ], ) diff --git a/integrations/openai-agents/python/examples/agents_examples/orchestrator.py b/integrations/openai-agents/python/examples/agents_examples/subagents.py similarity index 79% rename from integrations/openai-agents/python/examples/agents_examples/orchestrator.py rename to integrations/openai-agents/python/examples/agents_examples/subagents.py index 5606312d8d..8deb96a336 100644 --- a/integrations/openai-agents/python/examples/agents_examples/orchestrator.py +++ b/integrations/openai-agents/python/examples/agents_examples/subagents.py @@ -1,14 +1,17 @@ -"""Orchestrator — multi-agent via the SDK's agents-as-tools pattern. +"""Subagents — multi-agent via the SDK's agents-as-tools pattern. Complements :mod:`handoff`: there, control *transfers* to the specialist -(triage agent exits the conversation). Here the orchestrator stays in charge +(triage agent exits the conversation). Here the supervisor stays in charge and *calls* specialists as tools (``Agent.as_tool()``), possibly several in -one turn, then synthesizes their outputs itself. +one turn, then synthesizes their outputs itself — a call-and-return +delegation, not a handoff. Same shape as the LangGraph "subagents" showcase +demo (a supervisor calling child agents as tools and getting results back), +just built with the SDK's own ``as_tool()`` instead of a routing graph. On the AG-UI side each specialist invocation is an ordinary ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested agent's own model turns stay internal to the SDK and are not streamed as -separate messages, so the client sees one coherent orchestrator transcript. +separate messages, so the client sees one coherent supervisor transcript. """ from __future__ import annotations @@ -45,7 +48,7 @@ ) -def create_orchestrator_agent() -> Agent: +def create_subagents_agent() -> Agent: return Agent( name="orchestrator", model=DEFAULT_MODEL, From f5ca23c576e8e0b66b8589ca79391290edaf963c Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 12 Jul 2026 12:38:05 +0200 Subject: [PATCH 24/94] feat(translator): implement start/end custom event params in to_agui() with demo example --- apps/dojo/src/agents.ts | 1 + .../(v2)/custom_lifecycle_events/README.mdx | 38 ++++ .../(v2)/custom_lifecycle_events/page.tsx | 166 ++++++++++++++++++ apps/dojo/src/config.ts | 7 + apps/dojo/src/files.json | 22 ++- apps/dojo/src/menu.ts | 1 + apps/dojo/src/types/integration.ts | 1 + .../examples/agents_examples/__init__.py | 27 ++- .../custom_lifecycle_events.py | 52 ++++++ .../python/examples/translator_server.py | 17 +- .../src/ag_ui_openai_agents/translator.py | 31 ++++ 11 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx create mode 100644 integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 69d9c21ad3..3fa71d4fe9 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -643,6 +643,7 @@ export const agentsIntegrations = { tool_based_generative_ui: "tool_based_generative_ui", handoff: "handoff", subagents: "subagents", + custom_lifecycle_events: "custom_lifecycle_events", }, ), diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx new file mode 100644 index 0000000000..6709d48c35 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx @@ -0,0 +1,38 @@ +# 🪝 Custom Lifecycle Events + +## What This Demo Shows + +`AGUITranslator.to_agui()` takes two optional params — `start_custom_event` +and `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance +(anything else raises `TypeError`), default `None` (off). When set, one +`CUSTOM` event is emitted right after `RUN_STARTED`, another right before +`RUN_FINISHED`. This demo's server (`translator_server.py`) uses them to +report fake usage split the way a real bill would be: `input_usage` +(prompt tokens/cost — known before the model even runs) at the start, +`output_usage` (completion tokens/cost — known only once the run is done) +at the end — plain conversation otherwise, same agent as `agentic_chat`. + +## How to Interact + +- "Say hi in one sentence." +- "Tell me one interesting fact about space." + +Watch the usage card above the chat: the input slot fills in immediately, +the output slot fills in once the run finishes, then a total appears. + +## Technical Details + +- `build_input_usage_event()`/`build_output_usage_event()` + (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s; + `translator_server.py`'s route just calls them and passes the result to + `to_agui(start_custom_event=..., end_custom_event=...)` +- Both events are opaque to the translator — it only checks `isinstance(..., + CustomEvent)`, the `name`/`value` shape is entirely up to the caller +- Frontend reads them via the raw AG-UI client, not a CopilotKit renderer: + `useAgent({ agentId }).agent.subscribe({ onCustomEvent })` — `CUSTOM` + events with an arbitrary `name` aren't covered by `renderActivityMessages` + (that's for `ACTIVITY_SNAPSHOT`/`ACTIVITY_DELTA`) or + `renderCustomMessages` (that decorates existing chat messages), so this + goes straight to the subscriber API +- The token/cost numbers are fake — the point is the event bracketing, not + real usage accounting diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx new file mode 100644 index 0000000000..af47d621f6 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx @@ -0,0 +1,166 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { CopilotChat, useAgent, useConfigureSuggestions } from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; + +interface CustomLifecycleEventsProps { + params: Promise<{ integrationId: string }>; +} + +interface UsageInfo { + tokens: number; + costUsd: number; +} + +const CustomLifecycleEvents: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + ); +}; + +// These names/shapes are whatever the server chose when it built the +// CustomEvent — translator_server.py's custom_lifecycle_events route calls +// build_input_usage_event()/build_output_usage_event() +// (agents_examples/custom_lifecycle_events.py) and passes the result into +// to_agui()'s start_custom_event/end_custom_event params, so one CUSTOM +// event goes out right after RUN_STARTED, another right before +// RUN_FINISHED. Nothing about the AG-UI CUSTOM event type dictates this +// shape; it's just what this demo's server-side code picked. +const Chat = () => { + const { agent } = useAgent({ agentId: "custom_lifecycle_events" }); + const [inputUsage, setInputUsage] = useState(null); + const [outputUsage, setOutputUsage] = useState(null); + + useConfigureSuggestions({ + suggestions: [ + { title: "Say hi", message: "Say hi in one sentence." }, + { title: "Tell a fact", message: "Tell me one interesting fact about space." }, + ], + available: "always", + }); + + useEffect(() => { + const subscription = agent.subscribe({ + onRunStartedEvent: () => { + setInputUsage(null); + setOutputUsage(null); + }, + onCustomEvent: ({ event }) => { + if (event.name === "input_usage") { + const value = event.value as { tokens: number; cost_usd: number }; + setInputUsage({ tokens: value.tokens, costUsd: value.cost_usd }); + } + if (event.name === "output_usage") { + const value = event.value as { tokens: number; cost_usd: number }; + setOutputUsage({ tokens: value.tokens, costUsd: value.cost_usd }); + } + }, + }); + return () => subscription.unsubscribe(); + }, [agent]); + + return ( +
+
+ +
+
+ +
+
+ ); +}; + +function UsageMeter({ + inputUsage, + outputUsage, +}: { + inputUsage: UsageInfo | null; + outputUsage: UsageInfo | null; +}) { + if (!inputUsage && !outputUsage) { + return ( +
+ Send a message — a CUSTOM event reports fake input-token cost right after the run + starts, another reports output-token cost right before it finishes. +
+ ); + } + + const totalCost = (inputUsage?.costUsd ?? 0) + (outputUsage?.costUsd ?? 0); + + return ( +
+
+ + Usage (fake) + + {inputUsage && outputUsage && ( + + ${totalCost.toFixed(6)} total + + )} +
+ +
+ + +
+
+ ); +} + +function UsageSlot({ + label, + emoji, + usage, + pendingLabel, +}: { + label: string; + emoji: string; + usage: UsageInfo | null; + pendingLabel: string; +}) { + return ( +
+
+ {emoji} + {label} tokens +
+ {usage ? ( + <> +
+ {usage.tokens} +
+
+ ${usage.costUsd.toFixed(6)} +
+ + ) : ( +
+ {pendingLabel} +
+ )} +
+ ); +} + +export default CustomLifecycleEvents; diff --git a/apps/dojo/src/config.ts b/apps/dojo/src/config.ts index 2508af2d90..99ee02cb9f 100644 --- a/apps/dojo/src/config.ts +++ b/apps/dojo/src/config.ts @@ -102,6 +102,13 @@ export const featureConfig: FeatureConfig[] = [ "Supervisor agent calls specialist agents as tools and synthesizes the result", tags: ["Multi-agent architecture", "Sub-agents", "Orchestrator", "Agent as tool"], }), + createFeatureConfig({ + id: "custom_lifecycle_events", + name: "Custom Lifecycle Events", + description: + "Manual CUSTOM events bracket the run — a session marker right after start, a usage summary right before finish", + tags: ["Custom Events", "Lifecycle", "Translator"], + }), createFeatureConfig({ id: "a2a_chat", name: "A2A Chat", diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 503ecb1f99..8621a8ffbf 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4940,7 +4940,27 @@ }, { "name": "subagents.py", - "content": "\"\"\"Subagents — multi-agent via the SDK's agents-as-tools pattern.\n\nComplements :mod:`handoff`: there, control *transfers* to the specialist\n(triage agent exits the conversation). Here the supervisor stays in charge\nand *calls* specialists as tools (``Agent.as_tool()``), possibly several in\none turn, then synthesizes their outputs itself — a call-and-return\ndelegation, not a handoff. Same shape as the LangGraph \"subagents\" showcase\ndemo (a supervisor calling child agents as tools and getting results back),\njust built with the SDK's own ``as_tool()`` instead of a routing graph.\n\nOn the AG-UI side each specialist invocation is an ordinary\n``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested\nagent's own model turns stay internal to the SDK and are not streamed as\nseparate messages, so the client sees one coherent supervisor transcript.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\nresearch_agent = Agent(\n name=\"research_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You research a topic and return the key facts as a short bullet \"\n \"list. Facts only — no fluff, no conclusions.\"\n ),\n)\n\nwriter_agent = Agent(\n name=\"writer_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You turn bullet-point facts into short, engaging prose. One tight \"\n \"paragraph unless asked otherwise.\"\n ),\n)\n\ncritic_agent = Agent(\n name=\"critic_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You review a draft and return concrete improvement suggestions as a \"\n \"numbered list. Max 3 suggestions, be specific.\"\n ),\n)\n\n\ndef create_subagents_agent() -> Agent:\n return Agent(\n name=\"supervisor\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You supervise a small content team. For writing requests: \"\n \"call research_topic for the facts, then write_prose to draft, \"\n \"then critique_draft to review, then produce the final version \"\n \"yourself incorporating the critique. For simple questions, \"\n \"answer directly without the team.\"\n ),\n tools=[\n research_agent.as_tool(\n tool_name=\"research_topic\",\n tool_description=\"Research a topic and return key facts as bullets.\",\n ),\n writer_agent.as_tool(\n tool_name=\"write_prose\",\n tool_description=\"Turn bullet-point facts into short prose.\",\n ),\n critic_agent.as_tool(\n tool_name=\"critique_draft\",\n tool_description=\"Review a draft and suggest up to 3 improvements.\",\n ),\n ],\n )\n", + "content": "\"\"\"Subagents — multi-agent via the SDK's agents-as-tools pattern.\n\nComplements :mod:`handoff`: there, control *transfers* to the specialist\n(triage agent exits the conversation). Here the supervisor stays in charge\nand *calls* specialists as tools (``Agent.as_tool()``), possibly several in\none turn, then synthesizes their outputs itself — a call-and-return\ndelegation, not a handoff. Same shape as the LangGraph \"subagents\" showcase\ndemo (a supervisor calling child agents as tools and getting results back),\njust built with the SDK's own ``as_tool()`` instead of a routing graph.\n\nOn the AG-UI side each specialist invocation is an ordinary\n``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested\nagent's own model turns stay internal to the SDK and are not streamed as\nseparate messages, so the client sees one coherent supervisor transcript.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\nresearch_agent = Agent(\n name=\"research_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You research a topic and return the key facts as a short bullet \"\n \"list. Facts only — no fluff, no conclusions.\"\n ),\n)\n\nwriter_agent = Agent(\n name=\"writer_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You turn bullet-point facts into short, engaging prose. One tight \"\n \"paragraph unless asked otherwise.\"\n ),\n)\n\ncritic_agent = Agent(\n name=\"critic_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You review a draft and return concrete improvement suggestions as a \"\n \"numbered list. Max 3 suggestions, be specific.\"\n ),\n)\n\n\ndef create_subagents_agent() -> Agent:\n return Agent(\n name=\"orchestrator\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You orchestrate a small content team. For writing requests: \"\n \"call research_topic for the facts, then write_prose to draft, \"\n \"then critique_draft to review, then produce the final version \"\n \"yourself incorporating the critique. For simple questions, \"\n \"answer directly without the team.\"\n ),\n tools=[\n research_agent.as_tool(\n tool_name=\"research_topic\",\n tool_description=\"Research a topic and return key facts as bullets.\",\n ),\n writer_agent.as_tool(\n tool_name=\"write_prose\",\n tool_description=\"Turn bullet-point facts into short prose.\",\n ),\n critic_agent.as_tool(\n tool_name=\"critique_draft\",\n tool_description=\"Review a draft and suggest up to 3 improvements.\",\n ),\n ],\n )\n", + "language": "python", + "type": "file" + } + ], + "openai-agents-python::custom_lifecycle_events": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useEffect, useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useAgent, useConfigureSuggestions } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface UsageInfo {\n tokens: number;\n costUsd: number;\n}\n\nconst CustomLifecycleEvents: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\n// These names/shapes are whatever the server chose when it built the\n// CustomEvent — translator_server.py's custom_lifecycle_events route calls\n// build_input_usage_event()/build_output_usage_event()\n// (agents_examples/custom_lifecycle_events.py) and passes the result into\n// to_agui()'s start_custom_event/end_custom_event params, so one CUSTOM\n// event goes out right after RUN_STARTED, another right before\n// RUN_FINISHED. Nothing about the AG-UI CUSTOM event type dictates this\n// shape; it's just what this demo's server-side code picked.\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const [inputUsage, setInputUsage] = useState(null);\n const [outputUsage, setOutputUsage] = useState(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n { title: \"Tell a fact\", message: \"Tell me one interesting fact about space.\" },\n ],\n available: \"always\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onRunStartedEvent: () => {\n setInputUsage(null);\n setOutputUsage(null);\n },\n onCustomEvent: ({ event }) => {\n if (event.name === \"input_usage\") {\n const value = event.value as { tokens: number; cost_usd: number };\n setInputUsage({ tokens: value.tokens, costUsd: value.cost_usd });\n }\n if (event.name === \"output_usage\") {\n const value = event.value as { tokens: number; cost_usd: number };\n setOutputUsage({ tokens: value.tokens, costUsd: value.cost_usd });\n }\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n
\n \n
\n
\n \n
\n
\n );\n};\n\nfunction UsageMeter({\n inputUsage,\n outputUsage,\n}: {\n inputUsage: UsageInfo | null;\n outputUsage: UsageInfo | null;\n}) {\n if (!inputUsage && !outputUsage) {\n return (\n
\n Send a message — a CUSTOM event reports fake input-token cost right after the run\n starts, another reports output-token cost right before it finishes.\n
\n );\n }\n\n const totalCost = (inputUsage?.costUsd ?? 0) + (outputUsage?.costUsd ?? 0);\n\n return (\n
\n
\n \n Usage (fake)\n \n {inputUsage && outputUsage && (\n \n ${totalCost.toFixed(6)} total\n \n )}\n
\n\n
\n \n \n
\n
\n );\n}\n\nfunction UsageSlot({\n label,\n emoji,\n usage,\n pendingLabel,\n}: {\n label: string;\n emoji: string;\n usage: UsageInfo | null;\n pendingLabel: string;\n}) {\n return (\n \n
\n {emoji}\n {label} tokens\n
\n {usage ? (\n <>\n
\n {usage.tokens}\n
\n
\n ${usage.costUsd.toFixed(6)}\n
\n \n ) : (\n
\n {pendingLabel}\n
\n )}\n \n );\n}\n\nexport default CustomLifecycleEvents;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes two optional params — `start_custom_event`\nand `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance\n(anything else raises `TypeError`), default `None` (off). When set, one\n`CUSTOM` event is emitted right after `RUN_STARTED`, another right before\n`RUN_FINISHED`. This demo's server (`translator_server.py`) uses them to\nreport fake usage split the way a real bill would be: `input_usage`\n(prompt tokens/cost — known before the model even runs) at the start,\n`output_usage` (completion tokens/cost — known only once the run is done)\nat the end — plain conversation otherwise, same agent as `agentic_chat`.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about space.\"\n\nWatch the usage card above the chat: the input slot fills in immediately,\nthe output slot fills in once the run finishes, then a total appears.\n\n## Technical Details\n\n- `build_input_usage_event()`/`build_output_usage_event()`\n (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s;\n `translator_server.py`'s route just calls them and passes the result to\n `to_agui(start_custom_event=..., end_custom_event=...)`\n- Both events are opaque to the translator — it only checks `isinstance(...,\n CustomEvent)`, the `name`/`value` shape is entirely up to the caller\n- Frontend reads them via the raw AG-UI client, not a CopilotKit renderer:\n `useAgent({ agentId }).agent.subscribe({ onCustomEvent })` — `CUSTOM`\n events with an arbitrary `name` aren't covered by `renderActivityMessages`\n (that's for `ACTIVITY_SNAPSHOT`/`ACTIVITY_DELTA`) or\n `renderCustomMessages` (that decorates existing chat messages), so this\n goes straight to the subscriber API\n- The token/cost numbers are fake — the point is the event bracketing, not\n real usage accounting\n", + "language": "markdown", + "type": "file" + }, + { + "name": "custom_lifecycle_events.py", + "content": "\"\"\"Custom lifecycle events — manual to_agui() with a CUSTOM event bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. This ``DemoConfig`` sets\n``build_start_custom_event``/``build_end_custom_event`` (see\n``agents_examples/__init__.py``); the shared run loop in\ntranslator_server.py calls them and forwards the result into\n``to_agui()``'s ``start_custom_event``/``end_custom_event`` params, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_event() -> CustomEvent:\n tokens = random.randint(20, 120)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"input_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)},\n )\n\n\ndef build_output_usage_event() -> CustomEvent:\n tokens = random.randint(120, 480)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"output_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)},\n )\n", "language": "python", "type": "file" } diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index 94e1b54e18..1588cbc26f 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -387,6 +387,7 @@ export const menuIntegrations = [ "tool_based_generative_ui", "handoff", "subagents", + "custom_lifecycle_events", ], }, { diff --git a/apps/dojo/src/types/integration.ts b/apps/dojo/src/types/integration.ts index b255309134..5f1294f7a6 100644 --- a/apps/dojo/src/types/integration.ts +++ b/apps/dojo/src/types/integration.ts @@ -22,6 +22,7 @@ export type Feature = | "crew_chat" | "handoff" | "subagents" + | "custom_lifecycle_events" | "error_flow" | "background_agents" | "observational_memory"; diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py index 10006482a0..72415130a9 100644 --- a/integrations/openai-agents/python/examples/agents_examples/__init__.py +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -1,7 +1,15 @@ """Example agent factories for the AG-UI × OpenAI Agents SDK server. Each demo is a ``DemoConfig`` wrapping the SDK agent. ``build_registry()`` -assembles the full name → config map ``server.py`` serves, one route per key. +assembles the full name → config map both servers serve, one route per key. + +Most demos only set ``agent`` — translator_server.py's shared run loop calls +plain ``to_agui(result, body)`` for those. custom_lifecycle_events also sets +``build_start_custom_event``/``build_end_custom_event``: optional callables +returning a ``CustomEvent``, which the shared loop forwards into +``to_agui(..., start_custom_event=..., end_custom_event=...)`` when present. +No if-branch keyed by demo name, no separate router — one generic loop, +one optional per-demo hook. Stateful demos (shared_state, agentic_generative_ui, predictive_state_updates) are shelved together with ``AGUIContext`` — @@ -11,11 +19,18 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Callable from agents import Agent +from ag_ui.core import CustomEvent from .agentic_chat import create_agentic_chat_agent from .backend_tool_rendering import create_backend_tool_agent +from .custom_lifecycle_events import ( + build_input_usage_event, + build_output_usage_event, + create_custom_lifecycle_events_agent, +) from .handoff import create_handoff_agent from .human_in_the_loop import create_human_in_the_loop_agent from .subagents import create_subagents_agent @@ -24,9 +39,11 @@ @dataclass(frozen=True) class DemoConfig: - """One demo route: the agent to run for it.""" + """One demo route: the agent to run for it, plus optional lifecycle hooks.""" agent: Agent + build_start_custom_event: Callable[[], CustomEvent] | None = None + build_end_custom_event: Callable[[], CustomEvent] | None = None def build_registry() -> dict[str, DemoConfig]: @@ -38,6 +55,11 @@ def build_registry() -> dict[str, DemoConfig]: "tool_based_generative_ui": DemoConfig(agent=create_tool_based_generative_ui_agent()), "handoff": DemoConfig(agent=create_handoff_agent()), "subagents": DemoConfig(agent=create_subagents_agent()), + "custom_lifecycle_events": DemoConfig( + agent=create_custom_lifecycle_events_agent(), + build_start_custom_event=build_input_usage_event, + build_end_custom_event=build_output_usage_event, + ), } @@ -46,6 +68,7 @@ def build_registry() -> dict[str, DemoConfig]: "build_registry", "create_agentic_chat_agent", "create_backend_tool_agent", + "create_custom_lifecycle_events_agent", "create_handoff_agent", "create_human_in_the_loop_agent", "create_subagents_agent", diff --git a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py new file mode 100644 index 0000000000..0b68ddfe41 --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py @@ -0,0 +1,52 @@ +"""Custom lifecycle events — manual to_agui() with a CUSTOM event bracketing the run. + +Plain chat agent, same as agentic_chat — the point isn't the agent, it's +the two builder functions below. This ``DemoConfig`` sets +``build_start_custom_event``/``build_end_custom_event`` (see +``agents_examples/__init__.py``); the shared run loop in +translator_server.py calls them and forwards the result into +``to_agui()``'s ``start_custom_event``/``end_custom_event`` params, so one +CUSTOM event goes out right after RUN_STARTED and another right before +RUN_FINISHED. Only CustomEvent instances are accepted there; anything else +raises TypeError. + +input_usage fires at the start because prompt tokens/cost are known before +the model even runs; output_usage fires at the end because completion +tokens/cost are only known once the run is done. Numbers here are fake — +the point is the event bracketing, not real usage accounting. +""" + +from __future__ import annotations + +import random + +from ag_ui.core import CustomEvent, EventType +from agents import Agent + +from .constants import DEFAULT_MODEL + + +def create_custom_lifecycle_events_agent() -> Agent: + return Agent( + name="assistant", + model=DEFAULT_MODEL, + instructions="You are a helpful assistant. Be concise.", + ) + + +def build_input_usage_event() -> CustomEvent: + tokens = random.randint(20, 120) + return CustomEvent( + type=EventType.CUSTOM, + name="input_usage", + value={"tokens": tokens, "cost_usd": round(tokens * 0.00000015, 6)}, + ) + + +def build_output_usage_event() -> CustomEvent: + tokens = random.randint(120, 480) + return CustomEvent( + type=EventType.CUSTOM, + name="output_usage", + value={"tokens": tokens, "cost_usd": round(tokens * 0.0000006, 6)}, + ) diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index 36856e761e..a26e9db468 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -16,6 +16,10 @@ POST /tool_based_generative_ui ← frontend tool renders the content POST /handoff ← multi-agent triage via handoffs= POST /orchestrator ← multi-agent via agents-as-tools + POST /custom_lifecycle_events ← manual CUSTOM event right after RUN_STARTED + and right before RUN_FINISHED, via + DemoConfig.build_start_custom_event / + build_end_custom_event GET /health ← liveness check @@ -124,13 +128,24 @@ async def _stream(demo: DemoConfig, body: RunAgentInput): if translated.tools: agent = agent.clone(tools=[*agent.tools, *translated.tools]) + # Demo-specific lifecycle hooks (only custom_lifecycle_events sets these) — + # start_custom_event/end_custom_event only accept CustomEvent instances + # (anything else raises TypeError), so building them here, not inline, + # keeps this loop demo-agnostic: it just forwards whatever the registry + # gave it. + to_agui_kwargs = {} + if demo.build_start_custom_event: + to_agui_kwargs["start_custom_event"] = demo.build_start_custom_event() + if demo.build_end_custom_event: + to_agui_kwargs["end_custom_event"] = demo.build_end_custom_event() + # 2 — Run the agent; to_agui() wraps the stream with the lifecycle events # (RUN_STARTED first, RUN_FINISHED / RUN_ERROR last), echoes the state # snapshot, handles window bookkeeping + the final flush, and appends a # trailing MESSAGES_SNAPSHOT. Nothing to hand-emit here. try: result = Runner.run_streamed(agent, input=translated.messages) - async for ag_event in translator.to_agui(result, body): + async for ag_event in translator.to_agui(result, body, **to_agui_kwargs): yield encoder.encode(ag_event) except Exception: # to_agui already emitted RUN_ERROR before re-raising; just log here so diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 4bf0c85fcf..c06c06fd7a 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -18,6 +18,7 @@ from agents.stream_events import StreamEvent from ag_ui.core import ( BaseEvent, + CustomEvent, EventType, RunAgentInput, RunErrorEvent, @@ -80,6 +81,8 @@ async def to_agui( emit_run_error: bool = True, run_error_message: str | None = None, emit_state_snapshot: bool = True, + start_custom_event: CustomEvent | None = None, + end_custom_event: CustomEvent | None = None, ) -> AsyncIterator[BaseEvent]: """Translate an SDK event stream into a live AG-UI event stream. @@ -153,16 +156,41 @@ async def to_agui( emit_state_snapshot: Whether to echo run_input.state as a STATE_SNAPSHOT after RUN_STARTED when it isn't None. Defaults to True. + start_custom_event: An optional CustomEvent yielded right after + RUN_STARTED (before the STATE_SNAPSHOT), for callers who + need to signal something to the client before any run + content starts flowing. Must be a CustomEvent instance — + anything else raises TypeError. None (the default) emits + nothing. + end_custom_event: An optional CustomEvent yielded right before + RUN_FINISHED, after the MESSAGES_SNAPSHOT. Same type + restriction and default as start_custom_event. Yields: AG-UI BaseEvent instances, ready to encode. + + Raises: + TypeError: start_custom_event or end_custom_event was given but + is not a CustomEvent instance. """ + if start_custom_event and not isinstance(start_custom_event, CustomEvent): + raise TypeError( + f"start_custom_event must be a CustomEvent, got {type(start_custom_event).__name__}" + ) + if end_custom_event and not isinstance(end_custom_event, CustomEvent): + raise TypeError( + f"end_custom_event must be a CustomEvent, got {type(end_custom_event).__name__}" + ) + yield RunStartedEvent( type=EventType.RUN_STARTED, thread_id=run_input.thread_id, run_id=run_input.run_id, ) + if start_custom_event: + yield start_custom_event + if emit_state_snapshot and run_input.state is not None: yield StateSnapshotEvent( type=EventType.STATE_SNAPSHOT, @@ -207,6 +235,9 @@ async def to_agui( ) raise + if end_custom_event: + yield end_custom_event + yield RunFinishedEvent( type=EventType.RUN_FINISHED, thread_id=run_input.thread_id, From ad8d47980d67ad131e8f9e362a73fa40602aa480 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 12 Jul 2026 13:27:53 +0200 Subject: [PATCH 25/94] feat(dojo): show custom lifecycle event usage as per-message label --- .../(v2)/custom_lifecycle_events/README.mdx | 17 +- .../(v2)/custom_lifecycle_events/page.tsx | 225 +++++++++--------- apps/dojo/src/files.json | 4 +- 3 files changed, 124 insertions(+), 122 deletions(-) diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx index 6709d48c35..aae2b000a9 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx @@ -15,10 +15,11 @@ at the end — plain conversation otherwise, same agent as `agentic_chat`. ## How to Interact - "Say hi in one sentence." -- "Tell me one interesting fact about space." +- "Tell me one interesting fact about AI." -Watch the usage card above the chat: the input slot fills in immediately, -the output slot fills in once the run finishes, then a total appears. +Each assistant reply has a compact usage label immediately above it. The label +shows the input usage reported at the beginning of the run and the output usage +reported at the end. ## Technical Details @@ -28,11 +29,9 @@ the output slot fills in once the run finishes, then a total appears. `to_agui(start_custom_event=..., end_custom_event=...)` - Both events are opaque to the translator — it only checks `isinstance(..., CustomEvent)`, the `name`/`value` shape is entirely up to the caller -- Frontend reads them via the raw AG-UI client, not a CopilotKit renderer: - `useAgent({ agentId }).agent.subscribe({ onCustomEvent })` — `CUSTOM` - events with an arbitrary `name` aren't covered by `renderActivityMessages` - (that's for `ACTIVITY_SNAPSHOT`/`ACTIVITY_DELTA`) or - `renderCustomMessages` (that decorates existing chat messages), so this - goes straight to the subscriber API +- The page captures the current `runId` from `RUN_STARTED`, associates the + custom usage events with that run in a small external store, then uses + `CopilotKitProvider`'s stable `renderCustomMessages` configuration to render + the matching label above the assistant message - The token/cost numbers are fake — the point is the event bracketing, not real usage accounting diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx index af47d621f6..7d16223fdf 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx @@ -1,8 +1,18 @@ "use client"; -import React, { useEffect, useState } from "react"; + +import React, { + useEffect, + useMemo, + useRef, + useSyncExternalStore, +} from "react"; import "@copilotkit/react-core/v2/styles.css"; -import { CopilotChat, useAgent, useConfigureSuggestions } from "@copilotkit/react-core/v2"; -import { CopilotKit } from "@copilotkit/react-core"; +import { + CopilotChat, + CopilotKitProvider, + useAgent, + useConfigureSuggestions, +} from "@copilotkit/react-core/v2"; interface CustomLifecycleEventsProps { params: Promise<{ integrationId: string }>; @@ -13,67 +23,140 @@ interface UsageInfo { costUsd: number; } -const CustomLifecycleEvents: React.FC = ({ params }) => { +interface RunUsage { + input?: UsageInfo; + output?: UsageInfo; +} + +const runUsage = new Map(); +const usageListeners = new Set<() => void>(); + +function setRunUsage(runId: string, update: RunUsage) { + runUsage.set(runId, { ...runUsage.get(runId), ...update }); + usageListeners.forEach((listener) => listener()); +} + +function subscribeRunUsage(listener: () => void) { + usageListeners.add(listener); + return () => usageListeners.delete(listener); +} + +function getRunUsage(runId?: string) { + return runId ? runUsage.get(runId) : undefined; +} + +function UsageLabel({ + message, + position, + runId, +}: { + message: { role: string }; + position: "before" | "after"; + runId?: string; +}) { + const usage = useSyncExternalStore( + subscribeRunUsage, + () => getRunUsage(runId), + () => undefined, + ); + + if ( + message.role !== "assistant" || + position !== "after" || + (!usage?.input && !usage?.output) + ) { + return null; + } + + return ( +
+ {usage.input && ( + + ⬇️ {usage.input.tokens} tokens · ${usage.input.costUsd.toFixed(6)} + + )} + {usage.input && usage.output && } + {usage.output && ( + + ⬆️ {usage.output.tokens} tokens · ${usage.output.costUsd.toFixed(6)} + + )} +
+ ); +} + +const CustomLifecycleEvents: React.FC = ({ + params, +}) => { const { integrationId } = React.use(params); + const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []); return ( - - + ); }; -// These names/shapes are whatever the server chose when it built the -// CustomEvent — translator_server.py's custom_lifecycle_events route calls -// build_input_usage_event()/build_output_usage_event() -// (agents_examples/custom_lifecycle_events.py) and passes the result into -// to_agui()'s start_custom_event/end_custom_event params, so one CUSTOM -// event goes out right after RUN_STARTED, another right before -// RUN_FINISHED. Nothing about the AG-UI CUSTOM event type dictates this -// shape; it's just what this demo's server-side code picked. const Chat = () => { const { agent } = useAgent({ agentId: "custom_lifecycle_events" }); - const [inputUsage, setInputUsage] = useState(null); - const [outputUsage, setOutputUsage] = useState(null); + const currentRunId = useRef(undefined); useConfigureSuggestions({ suggestions: [ { title: "Say hi", message: "Say hi in one sentence." }, - { title: "Tell a fact", message: "Tell me one interesting fact about space." }, + { + title: "Tell me a fact about AI", + message: "Tell me one interesting fact about AI.", + }, ], - available: "always", + available: "before-first-message", + consumerAgentId: "custom_lifecycle_events", }); useEffect(() => { const subscription = agent.subscribe({ - onRunStartedEvent: () => { - setInputUsage(null); - setOutputUsage(null); + onRunStartedEvent: ({ event }) => { + currentRunId.current = event.runId; + runUsage.delete(event.runId); + usageListeners.forEach((listener) => listener()); }, onCustomEvent: ({ event }) => { + const runId = currentRunId.current; + if (!runId) return; + + const value = event.value as { tokens: number; cost_usd: number }; if (event.name === "input_usage") { - const value = event.value as { tokens: number; cost_usd: number }; - setInputUsage({ tokens: value.tokens, costUsd: value.cost_usd }); + setRunUsage(runId, { + input: { tokens: value.tokens, costUsd: value.cost_usd }, + }); } if (event.name === "output_usage") { - const value = event.value as { tokens: number; cost_usd: number }; - setOutputUsage({ tokens: value.tokens, costUsd: value.cost_usd }); + setRunUsage(runId, { + output: { tokens: value.tokens, costUsd: value.cost_usd }, + }); } }, + onRunFinishedEvent: () => { + currentRunId.current = undefined; + }, + onRunErrorEvent: () => { + currentRunId.current = undefined; + }, }); return () => subscription.unsubscribe(); }, [agent]); return ( -
-
- -
-
+
+
{ ); }; -function UsageMeter({ - inputUsage, - outputUsage, -}: { - inputUsage: UsageInfo | null; - outputUsage: UsageInfo | null; -}) { - if (!inputUsage && !outputUsage) { - return ( -
- Send a message — a CUSTOM event reports fake input-token cost right after the run - starts, another reports output-token cost right before it finishes. -
- ); - } - - const totalCost = (inputUsage?.costUsd ?? 0) + (outputUsage?.costUsd ?? 0); - - return ( -
-
- - Usage (fake) - - {inputUsage && outputUsage && ( - - ${totalCost.toFixed(6)} total - - )} -
- -
- - -
-
- ); -} - -function UsageSlot({ - label, - emoji, - usage, - pendingLabel, -}: { - label: string; - emoji: string; - usage: UsageInfo | null; - pendingLabel: string; -}) { - return ( -
-
- {emoji} - {label} tokens -
- {usage ? ( - <> -
- {usage.tokens} -
-
- ${usage.costUsd.toFixed(6)} -
- - ) : ( -
- {pendingLabel} -
- )} -
- ); -} - export default CustomLifecycleEvents; diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 8621a8ffbf..515b663232 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4948,13 +4948,13 @@ "openai-agents-python::custom_lifecycle_events": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useEffect, useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useAgent, useConfigureSuggestions } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface UsageInfo {\n tokens: number;\n costUsd: number;\n}\n\nconst CustomLifecycleEvents: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\n// These names/shapes are whatever the server chose when it built the\n// CustomEvent — translator_server.py's custom_lifecycle_events route calls\n// build_input_usage_event()/build_output_usage_event()\n// (agents_examples/custom_lifecycle_events.py) and passes the result into\n// to_agui()'s start_custom_event/end_custom_event params, so one CUSTOM\n// event goes out right after RUN_STARTED, another right before\n// RUN_FINISHED. Nothing about the AG-UI CUSTOM event type dictates this\n// shape; it's just what this demo's server-side code picked.\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const [inputUsage, setInputUsage] = useState(null);\n const [outputUsage, setOutputUsage] = useState(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n { title: \"Tell a fact\", message: \"Tell me one interesting fact about space.\" },\n ],\n available: \"always\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onRunStartedEvent: () => {\n setInputUsage(null);\n setOutputUsage(null);\n },\n onCustomEvent: ({ event }) => {\n if (event.name === \"input_usage\") {\n const value = event.value as { tokens: number; cost_usd: number };\n setInputUsage({ tokens: value.tokens, costUsd: value.cost_usd });\n }\n if (event.name === \"output_usage\") {\n const value = event.value as { tokens: number; cost_usd: number };\n setOutputUsage({ tokens: value.tokens, costUsd: value.cost_usd });\n }\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n
\n \n
\n
\n \n
\n
\n );\n};\n\nfunction UsageMeter({\n inputUsage,\n outputUsage,\n}: {\n inputUsage: UsageInfo | null;\n outputUsage: UsageInfo | null;\n}) {\n if (!inputUsage && !outputUsage) {\n return (\n
\n Send a message — a CUSTOM event reports fake input-token cost right after the run\n starts, another reports output-token cost right before it finishes.\n
\n );\n }\n\n const totalCost = (inputUsage?.costUsd ?? 0) + (outputUsage?.costUsd ?? 0);\n\n return (\n
\n
\n \n Usage (fake)\n \n {inputUsage && outputUsage && (\n \n ${totalCost.toFixed(6)} total\n \n )}\n
\n\n
\n \n \n
\n
\n );\n}\n\nfunction UsageSlot({\n label,\n emoji,\n usage,\n pendingLabel,\n}: {\n label: string;\n emoji: string;\n usage: UsageInfo | null;\n pendingLabel: string;\n}) {\n return (\n \n
\n {emoji}\n {label} tokens\n
\n {usage ? (\n <>\n
\n {usage.tokens}\n
\n
\n ${usage.costUsd.toFixed(6)}\n
\n \n ) : (\n
\n {pendingLabel}\n
\n )}\n
\n );\n}\n\nexport default CustomLifecycleEvents;\n", + "content": "\"use client\";\n\nimport React, {\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface UsageInfo {\n tokens: number;\n costUsd: number;\n}\n\ninterface RunUsage {\n input?: UsageInfo;\n output?: UsageInfo;\n}\n\nconst runUsage = new Map();\nconst usageListeners = new Set<() => void>();\n\nfunction setRunUsage(runId: string, update: RunUsage) {\n runUsage.set(runId, { ...runUsage.get(runId), ...update });\n usageListeners.forEach((listener) => listener());\n}\n\nfunction subscribeRunUsage(listener: () => void) {\n usageListeners.add(listener);\n return () => usageListeners.delete(listener);\n}\n\nfunction getRunUsage(runId?: string) {\n return runId ? runUsage.get(runId) : undefined;\n}\n\nfunction UsageLabel({ runId }: { runId?: string }) {\n const usage = useSyncExternalStore(\n subscribeRunUsage,\n () => getRunUsage(runId),\n () => undefined,\n );\n\n if (!usage?.input && !usage?.output) {\n return null;\n }\n\n return (\n \n {usage.input && (\n \n ⬇️ {usage.input.tokens} tok · ${usage.input.costUsd.toFixed(6)}\n \n )}\n {usage.input && usage.output && }\n {usage.output && (\n \n ⬆️ {usage.output.tokens} tok · ${usage.output.costUsd.toFixed(6)}\n \n )}\n
\n );\n}\n\nconst CustomLifecycleEvents: React.FC = ({\n params,\n}) => {\n const { integrationId } = React.use(params);\n const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const currentRunId = useRef(undefined);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n {\n title: \"Tell a fact\",\n message: \"Tell me one interesting fact about space.\",\n },\n ],\n available: \"always\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onRunStartedEvent: ({ event }) => {\n currentRunId.current = event.runId;\n runUsage.delete(event.runId);\n usageListeners.forEach((listener) => listener());\n },\n onCustomEvent: ({ event }) => {\n const runId = currentRunId.current;\n if (!runId) return;\n\n const value = event.value as { tokens: number; cost_usd: number };\n if (event.name === \"input_usage\") {\n setRunUsage(runId, {\n input: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n if (event.name === \"output_usage\") {\n setRunUsage(runId, {\n output: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n },\n onRunFinishedEvent: () => {\n currentRunId.current = undefined;\n },\n onRunErrorEvent: () => {\n currentRunId.current = undefined;\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default CustomLifecycleEvents;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes two optional params — `start_custom_event`\nand `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance\n(anything else raises `TypeError`), default `None` (off). When set, one\n`CUSTOM` event is emitted right after `RUN_STARTED`, another right before\n`RUN_FINISHED`. This demo's server (`translator_server.py`) uses them to\nreport fake usage split the way a real bill would be: `input_usage`\n(prompt tokens/cost — known before the model even runs) at the start,\n`output_usage` (completion tokens/cost — known only once the run is done)\nat the end — plain conversation otherwise, same agent as `agentic_chat`.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about space.\"\n\nWatch the usage card above the chat: the input slot fills in immediately,\nthe output slot fills in once the run finishes, then a total appears.\n\n## Technical Details\n\n- `build_input_usage_event()`/`build_output_usage_event()`\n (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s;\n `translator_server.py`'s route just calls them and passes the result to\n `to_agui(start_custom_event=..., end_custom_event=...)`\n- Both events are opaque to the translator — it only checks `isinstance(...,\n CustomEvent)`, the `name`/`value` shape is entirely up to the caller\n- Frontend reads them via the raw AG-UI client, not a CopilotKit renderer:\n `useAgent({ agentId }).agent.subscribe({ onCustomEvent })` — `CUSTOM`\n events with an arbitrary `name` aren't covered by `renderActivityMessages`\n (that's for `ACTIVITY_SNAPSHOT`/`ACTIVITY_DELTA`) or\n `renderCustomMessages` (that decorates existing chat messages), so this\n goes straight to the subscriber API\n- The token/cost numbers are fake — the point is the event bracketing, not\n real usage accounting\n", + "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes two optional params — `start_custom_event`\nand `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance\n(anything else raises `TypeError`), default `None` (off). When set, one\n`CUSTOM` event is emitted right after `RUN_STARTED`, another right before\n`RUN_FINISHED`. This demo's server (`translator_server.py`) uses them to\nreport fake usage split the way a real bill would be: `input_usage`\n(prompt tokens/cost — known before the model even runs) at the start,\n`output_usage` (completion tokens/cost — known only once the run is done)\nat the end — plain conversation otherwise, same agent as `agentic_chat`.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about space.\"\n\nEach assistant reply has a compact usage label immediately above it. The label\nshows the input usage reported at the beginning of the run and the output usage\nreported at the end.\n\n## Technical Details\n\n- `build_input_usage_event()`/`build_output_usage_event()`\n (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s;\n `translator_server.py`'s route just calls them and passes the result to\n `to_agui(start_custom_event=..., end_custom_event=...)`\n- Both events are opaque to the translator — it only checks `isinstance(...,\n CustomEvent)`, the `name`/`value` shape is entirely up to the caller\n- The page captures the current `runId` from `RUN_STARTED`, associates the\n custom usage events with that run in a small external store, then uses\n `CopilotKitProvider`'s stable `renderCustomMessages` configuration to render\n the matching label above the assistant message\n- The token/cost numbers are fake — the point is the event bracketing, not\n real usage accounting\n", "language": "markdown", "type": "file" }, From b22d4137a1bd1aa997732d31a08d6468d735776d Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 12 Jul 2026 14:49:15 +0200 Subject: [PATCH 26/94] feat(translator): add initial and final state snapshot options to to_agui() --- integrations/openai-agents/python/README.md | 14 ++- .../src/ag_ui_openai_agents/translator.py | 87 ++++++++++++++----- .../python/tests/test_translators.py | 64 ++++++++++++-- 3 files changed, 135 insertions(+), 30 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 071ca7966e..6e54d40a35 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -104,8 +104,18 @@ curl -N -X POST http://localhost:8000 \ }' ``` -Expected: `RUN_STARTED -> TEXT_MESSAGE_START -> TEXT_MESSAGE_CONTENT (xN) -> -TEXT_MESSAGE_END -> RUN_FINISHED`. +Expected: `RUN_STARTED -> STATE_SNAPSHOT -> TEXT_MESSAGE_START -> +TEXT_MESSAGE_CONTENT (xN) -> TEXT_MESSAGE_END -> STATE_SNAPSHOT -> +MESSAGES_SNAPSHOT -> RUN_FINISHED`. + +`run_input.state` is echoed as a `STATE_SNAPSHOT` twice — once right after +`RUN_STARTED` (`emit_initial_state`) and once just before `MESSAGES_SNAPSHOT` +(`emit_final_state`) — both defaulting on, both gated only on +`state is not None` (an empty `{}` still emits; only `null` is skipped). The +OpenAI Agents SDK has no shared-state channel, so nothing mutates state +mid-run; the final echo is the settled-state slot the frontend can rely on +ending with. Pass `emit_initial_state=False` / `emit_final_state=False` to +`to_agui` to suppress either. A full multi-demo server (chat, backend tools, human-in-the-loop, handoffs, orchestrator) lives in [`examples/`](examples/). diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index c06c06fd7a..16169a3096 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -77,15 +77,30 @@ async def to_agui( events: RunResultStreaming | AsyncIterator[StreamEvent], run_input: RunAgentInput, *, + start_custom_event: CustomEvent | None = None, + emit_initial_state: bool = True, + emit_final_state: bool = True, emit_messages_snapshot: bool = True, + end_custom_event: CustomEvent | None = None, emit_run_error: bool = True, run_error_message: str | None = None, - emit_state_snapshot: bool = True, - start_custom_event: CustomEvent | None = None, - end_custom_event: CustomEvent | None = None, ) -> AsyncIterator[BaseEvent]: """Translate an SDK event stream into a live AG-UI event stream. + Canonical event order (the lifecycle events this method controls + directly; streamed message/tool/reasoning/step events flow through + outbound.translate between #3 and #4): + + 1. RUN_STARTED — always first + 2. start_custom_event — optional, if provided + 3. STATE_SNAPSHOT (initial) — echo run_input.state; emit_initial_state + … streamed STEP/TEXT/TOOL/REASONING events … + 4. STEP_FINISHED — final window close (outbound.finalize) + 5. STATE_SNAPSHOT (final) — run_input.state again; emit_final_state + 6. MESSAGES_SNAPSHOT — emit_messages_snapshot + 7. end_custom_event — optional, if provided + 8. RUN_FINISHED — always last (or RUN_ERROR on raise) + Feed the result from Runner.run_streamed, or result.stream_events(). A fresh stateful engine handles this run's windows; when the stream ends the engine flush runs automatically — any still-open text / @@ -118,12 +133,17 @@ async def to_agui( object or a bare stream_events() iterator — the snapshot is built from engine state, not result.new_items. - Right after RUN_STARTED, a STATE_SNAPSHOT echoes run_input.state - back whenever it isn't None — the same thing the other AG-UI - integrations that have no native SDK state do (the OpenAI Agents - SDK has no shared-state channel of its own; state lives at the - AG-UI/frontend layer). Pass emit_state_snapshot=False to suppress - it. Nothing state-related is emitted on the error path. + State is echoed as a STATE_SNAPSHOT twice, both gated on + run_input.state is not None (so an empty {} still emits — matching + aws-strands / claude-agent-sdk, and matching the frontend, whose + own `if (event.state)` guard treats {} as truthy but null as + skip). Once right after RUN_STARTED (emit_initial_state) and once + near the end, just before MESSAGES_SNAPSHOT (emit_final_state). + The OpenAI Agents SDK has no shared-state channel of its own, so + nothing mutates state mid-run; the final echo is the settled-state + slot the frontend can rely on ending with (and the hook where + future run-end state, e.g. handoff continuity, would land). + Nothing state-related is emitted on the error path. Note: run_input.messages passes through to the snapshot as-is. If it @@ -145,26 +165,29 @@ async def to_agui( thread_id/run_id for the lifecycle events and, unless emit_messages_snapshot=False, the snapshot's prior-history half. - emit_messages_snapshot: Whether to append a MESSAGES_SNAPSHOT - just before RUN_FINISHED. Defaults to True. - emit_run_error: Whether to yield a RUN_ERROR event when the - stream raises, before re-raising. Defaults to True; set - False only if you emit your own terminal error event. - run_error_message: The RUN_ERROR message. Defaults to None, which - sends str(exc); set a fixed string to keep raw exception - text off the wire. - emit_state_snapshot: Whether to echo run_input.state as a - STATE_SNAPSHOT after RUN_STARTED when it isn't None. - Defaults to True. start_custom_event: An optional CustomEvent yielded right after RUN_STARTED (before the STATE_SNAPSHOT), for callers who need to signal something to the client before any run content starts flowing. Must be a CustomEvent instance — anything else raises TypeError. None (the default) emits nothing. + emit_initial_state: Whether to echo run_input.state as a + STATE_SNAPSHOT right after RUN_STARTED when it isn't None. + Empty {} still emits; only None is skipped. Defaults to True. + emit_final_state: Whether to echo run_input.state as a + STATE_SNAPSHOT near the end, just before MESSAGES_SNAPSHOT, + when it isn't None. Same None-only gate. Defaults to True. + emit_messages_snapshot: Whether to append a MESSAGES_SNAPSHOT + just before RUN_FINISHED. Defaults to True. end_custom_event: An optional CustomEvent yielded right before RUN_FINISHED, after the MESSAGES_SNAPSHOT. Same type restriction and default as start_custom_event. + emit_run_error: Whether to yield a RUN_ERROR event when the + stream raises, before re-raising. Defaults to True; set + False only if you emit your own terminal error event. + run_error_message: The RUN_ERROR message. Defaults to None, which + sends str(exc); set a fixed string to keep raw exception + text off the wire. Yields: AG-UI BaseEvent instances, ready to encode. @@ -182,16 +205,19 @@ async def to_agui( f"end_custom_event must be a CustomEvent, got {type(end_custom_event).__name__}" ) + # 1. RUN_STARTED — always first yield RunStartedEvent( type=EventType.RUN_STARTED, thread_id=run_input.thread_id, run_id=run_input.run_id, ) + # 2. start_custom_event — optional if start_custom_event: yield start_custom_event - if emit_state_snapshot and run_input.state is not None: + # 3. STATE_SNAPSHOT (initial) — echo run_input.state ({} emits, None skips) + if emit_initial_state and run_input.state is not None: yield StateSnapshotEvent( type=EventType.STATE_SNAPSHOT, snapshot=run_input.state, @@ -204,11 +230,20 @@ async def to_agui( ) outbound = self._outbound_cls() try: + # … streamed STEP / TEXT / TOOL / REASONING events … async for sdk_event in stream_events: for event in outbound.translate(sdk_event): yield event + # 4. STEP_FINISHED — final window close for event in outbound.finalize(): yield event + # 5. STATE_SNAPSHOT (final) — settled state ({} emits, None skips) + if emit_final_state and run_input.state is not None: + yield StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=run_input.state, + ) + # 6. MESSAGES_SNAPSHOT if emit_messages_snapshot: yield outbound.build_messages_snapshot(run_input) except ClientToolPending: @@ -219,8 +254,16 @@ async def to_agui( # translate() dispatch above (the run-item event that names the # call arrives before the SDK ever invokes it), so this is a # clean end of turn: finalize any open windows, snapshot, finish. + # 4. STEP_FINISHED — final window close for event in outbound.finalize(): yield event + # 5. STATE_SNAPSHOT (final) — settled state ({} emits, None skips) + if emit_final_state and run_input.state is not None: + yield StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=run_input.state, + ) + # 6. MESSAGES_SNAPSHOT if emit_messages_snapshot: yield outbound.build_messages_snapshot(run_input) except (Exception, asyncio.CancelledError) as exc: @@ -235,9 +278,11 @@ async def to_agui( ) raise + # 7. end_custom_event — optional if end_custom_event: yield end_custom_event + # 8. RUN_FINISHED — always last yield RunFinishedEvent( type=EventType.RUN_FINISHED, thread_id=run_input.thread_id, diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index 258b437c94..2d6c076dd5 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -122,7 +122,8 @@ async def collect(): _fake_stream("a", "b"), _run_input(), emit_messages_snapshot=False, - emit_state_snapshot=False, + emit_initial_state=False, + emit_final_state=False, ) ] @@ -161,7 +162,7 @@ async def collect(): return [ e async for e in translator.to_agui( - result, _run_input(), emit_state_snapshot=False + result, _run_input(), emit_initial_state=False, emit_final_state=False ) ] @@ -342,7 +343,10 @@ async def collect(): return [ e async for e in translator.to_agui( - _fake_stream("a"), run_input, emit_messages_snapshot=False + _fake_stream("a"), + run_input, + emit_messages_snapshot=False, + emit_final_state=False, ) ] @@ -350,7 +354,7 @@ async def collect(): assert isinstance(events[0], RunStartedEvent) assert isinstance(events[1], StateSnapshotEvent) assert events[1].snapshot == {"theme": "dark"} - # Just the one snapshot — no mid-run state tracking. + # emit_final_state=False here isolates the initial echo — just the one. assert sum(isinstance(e, StateSnapshotEvent) for e in events) == 1 @@ -363,7 +367,10 @@ async def collect(): return [ e async for e in translator.to_agui( - _fake_stream("a"), _run_input(), emit_messages_snapshot=False + _fake_stream("a"), + _run_input(), + emit_messages_snapshot=False, + emit_final_state=False, ) ] @@ -373,7 +380,7 @@ async def collect(): assert snapshots[0].snapshot == {} -def test_to_agui_emit_state_snapshot_false_suppresses_it(): +def test_to_agui_state_flags_false_suppress_both_snapshots(): translator = AGUITranslator(outbound_cls=_StubOutbound) run_input = _run_input().model_copy(update={"state": {"x": 1}}) @@ -384,9 +391,52 @@ async def collect(): _fake_stream("a"), run_input, emit_messages_snapshot=False, - emit_state_snapshot=False, + emit_initial_state=False, + emit_final_state=False, ) ] events = asyncio.run(collect()) assert not any(isinstance(e, StateSnapshotEvent) for e in events) + + +def test_to_agui_emits_final_state_before_messages_snapshot(): + # Default flags: state echoes twice — initial right after RUN_STARTED and + # final in the settled-state slot (#5), just before MESSAGES_SNAPSHOT. + translator = AGUITranslator(outbound_cls=_StubOutbound) + run_input = _run_input().model_copy(update={"state": {"theme": "dark"}}) + + async def collect(): + return [e async for e in translator.to_agui(_fake_stream("a"), run_input)] + + events = asyncio.run(collect()) + snapshots = [e for e in events if isinstance(e, StateSnapshotEvent)] + # Two snapshots: initial echo + final settled state, both == run_input.state. + assert len(snapshots) == 2 + assert all(s.snapshot == {"theme": "dark"} for s in snapshots) + # Canonical order #5 → #6 → #8: final STATE, then MESSAGES, then RUN_FINISHED. + assert isinstance(events[-1], RunFinishedEvent) + assert isinstance(events[-2], MessagesSnapshotEvent) + assert events[-3] is snapshots[-1] + + +def test_to_agui_emit_final_state_false_keeps_only_initial(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + run_input = _run_input().model_copy(update={"state": {"x": 1}}) + + async def collect(): + return [ + e + async for e in translator.to_agui( + _fake_stream("a"), + run_input, + emit_messages_snapshot=False, + emit_final_state=False, + ) + ] + + events = asyncio.run(collect()) + snapshots = [e for e in events if isinstance(e, StateSnapshotEvent)] + assert len(snapshots) == 1 + # The lone snapshot is the initial echo (right after RUN_STARTED), not a final. + assert isinstance(events[1], StateSnapshotEvent) From f275c7b2c30b6ec2075349153c15f774c4e68f39 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 12 Jul 2026 15:14:17 +0200 Subject: [PATCH 27/94] chore(openai-agents): add AGENTS.md to .gitignore --- integrations/openai-agents/python/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/integrations/openai-agents/python/.gitignore b/integrations/openai-agents/python/.gitignore index 8ecaed6434..1749724d93 100644 --- a/integrations/openai-agents/python/.gitignore +++ b/integrations/openai-agents/python/.gitignore @@ -223,3 +223,4 @@ __marimo__/ # Dev .dev/ CLAUDE.md +AGENTS.md From 12ebfe966f7648c632daca0738a76b5cf8ad24e7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 12 Jul 2026 17:52:37 +0200 Subject: [PATCH 28/94] feat(translator): update state handling in to_agui() to accept static and async sources --- .../src/ag_ui_openai_agents/translator.py | 95 +++++++++++-------- .../python/tests/test_translators.py | 87 ++++++++++++++--- 2 files changed, 127 insertions(+), 55 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 16169a3096..6148c1e734 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -12,10 +12,12 @@ from __future__ import annotations import asyncio -from typing import AsyncIterator +import inspect +from typing import Any, AsyncIterator from agents.result import RunResultStreaming from agents.stream_events import StreamEvent + from ag_ui.core import ( BaseEvent, CustomEvent, @@ -26,7 +28,6 @@ RunStartedEvent, StateSnapshotEvent, ) - from .engine.agui_to_sdk import AGUIToSDKTranslator, ClientToolPending from .engine.sdk_to_agui import SDKToAGUITranslator from .engine.types import TranslatedInput @@ -78,8 +79,8 @@ async def to_agui( run_input: RunAgentInput, *, start_custom_event: CustomEvent | None = None, - emit_initial_state: bool = True, - emit_final_state: bool = True, + initial_state: Any = None, + final_state: Any = None, emit_messages_snapshot: bool = True, end_custom_event: CustomEvent | None = None, emit_run_error: bool = True, @@ -93,10 +94,10 @@ async def to_agui( 1. RUN_STARTED — always first 2. start_custom_event — optional, if provided - 3. STATE_SNAPSHOT (initial) — echo run_input.state; emit_initial_state + 3. STATE_SNAPSHOT (initial) — resolve initial_state … streamed STEP/TEXT/TOOL/REASONING events … 4. STEP_FINISHED — final window close (outbound.finalize) - 5. STATE_SNAPSHOT (final) — run_input.state again; emit_final_state + 5. STATE_SNAPSHOT (final) — resolve final_state 6. MESSAGES_SNAPSHOT — emit_messages_snapshot 7. end_custom_event — optional, if provided 8. RUN_FINISHED — always last (or RUN_ERROR on raise) @@ -133,17 +134,14 @@ async def to_agui( object or a bare stream_events() iterator — the snapshot is built from engine state, not result.new_items. - State is echoed as a STATE_SNAPSHOT twice, both gated on - run_input.state is not None (so an empty {} still emits — matching - aws-strands / claude-agent-sdk, and matching the frontend, whose - own `if (event.state)` guard treats {} as truthy but null as - skip). Once right after RUN_STARTED (emit_initial_state) and once - near the end, just before MESSAGES_SNAPSHOT (emit_final_state). - The OpenAI Agents SDK has no shared-state channel of its own, so - nothing mutates state mid-run; the final echo is the settled-state - slot the frontend can rely on ending with (and the hook where - future run-end state, e.g. handoff continuity, would land). - Nothing state-related is emitted on the error path. + State sources may be static values, zero-argument functions, or + zero-argument async functions. initial_state is resolved right after + RUN_STARTED; final_state is resolved near the end, after the SDK + stream and engine flush. Omit either argument to use run_input.state, + or pass None to suppress that snapshot. Empty {} still emits. This + lets application hooks and tools mutate state during a run while the + translator remains unaware of the state shape. Nothing state-related + is emitted on the error path. Note: run_input.messages passes through to the snapshot as-is. If it @@ -171,12 +169,14 @@ async def to_agui( content starts flowing. Must be a CustomEvent instance — anything else raises TypeError. None (the default) emits nothing. - emit_initial_state: Whether to echo run_input.state as a - STATE_SNAPSHOT right after RUN_STARTED when it isn't None. - Empty {} still emits; only None is skipped. Defaults to True. - emit_final_state: Whether to echo run_input.state as a - STATE_SNAPSHOT near the end, just before MESSAGES_SNAPSHOT, - when it isn't None. Same None-only gate. Defaults to True. + initial_state: State source resolved right after RUN_STARTED. + Accepts a static value, a zero-argument function, or a + zero-argument async function. Omitted uses run_input.state; + None suppresses the initial snapshot. + final_state: State source resolved after successful streaming and + finalization, just before MESSAGES_SNAPSHOT. Accepts the same + forms as initial_state. Omitted uses run_input.state; None + suppresses the final snapshot. emit_messages_snapshot: Whether to append a MESSAGES_SNAPSHOT just before RUN_FINISHED. Defaults to True. end_custom_event: An optional CustomEvent yielded right before @@ -216,12 +216,14 @@ async def to_agui( if start_custom_event: yield start_custom_event - # 3. STATE_SNAPSHOT (initial) — echo run_input.state ({} emits, None skips) - if emit_initial_state and run_input.state is not None: - yield StateSnapshotEvent( - type=EventType.STATE_SNAPSHOT, - snapshot=run_input.state, - ) + # 3. STATE_SNAPSHOT (initial) — resolve now ({} emits, None skips) + if initial_state is not None: + snapshot = await self._resolve_state_snapshot(initial_state) + if snapshot is not None: + yield StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=snapshot, + ) stream_events = ( events.stream_events() @@ -237,12 +239,14 @@ async def to_agui( # 4. STEP_FINISHED — final window close for event in outbound.finalize(): yield event - # 5. STATE_SNAPSHOT (final) — settled state ({} emits, None skips) - if emit_final_state and run_input.state is not None: - yield StateSnapshotEvent( - type=EventType.STATE_SNAPSHOT, - snapshot=run_input.state, - ) + # 5. STATE_SNAPSHOT (final) — resolve now ({} emits, None skips) + if final_state is not None: + snapshot = await self._resolve_state_snapshot(final_state) + if snapshot is not None: + yield StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=snapshot, + ) # 6. MESSAGES_SNAPSHOT if emit_messages_snapshot: yield outbound.build_messages_snapshot(run_input) @@ -257,12 +261,14 @@ async def to_agui( # 4. STEP_FINISHED — final window close for event in outbound.finalize(): yield event - # 5. STATE_SNAPSHOT (final) — settled state ({} emits, None skips) - if emit_final_state and run_input.state is not None: - yield StateSnapshotEvent( - type=EventType.STATE_SNAPSHOT, - snapshot=run_input.state, - ) + # 5. STATE_SNAPSHOT (final) — resolve now ({} emits, None skips) + if final_state is not None: + snapshot = await self._resolve_state_snapshot(final_state) + if snapshot is not None: + yield StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=snapshot, + ) # 6. MESSAGES_SNAPSHOT if emit_messages_snapshot: yield outbound.build_messages_snapshot(run_input) @@ -288,3 +294,10 @@ async def to_agui( thread_id=run_input.thread_id, run_id=run_input.run_id, ) + + async def _resolve_state_snapshot(self, state: Any) -> Any: + """Resolve a static, synchronous, or asynchronous state source.""" + value = state() if callable(state) else state + if inspect.isawaitable(value): + value = await value + return value \ No newline at end of file diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index 2d6c076dd5..f79630934d 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -122,8 +122,8 @@ async def collect(): _fake_stream("a", "b"), _run_input(), emit_messages_snapshot=False, - emit_initial_state=False, - emit_final_state=False, + initial_state=None, + final_state=None, ) ] @@ -162,7 +162,7 @@ async def collect(): return [ e async for e in translator.to_agui( - result, _run_input(), emit_initial_state=False, emit_final_state=False + result, _run_input(), initial_state=None, final_state=None ) ] @@ -346,7 +346,8 @@ async def collect(): _fake_stream("a"), run_input, emit_messages_snapshot=False, - emit_final_state=False, + initial_state=run_input.state, + final_state=None, ) ] @@ -354,7 +355,7 @@ async def collect(): assert isinstance(events[0], RunStartedEvent) assert isinstance(events[1], StateSnapshotEvent) assert events[1].snapshot == {"theme": "dark"} - # emit_final_state=False here isolates the initial echo — just the one. + # final_state=None here isolates the initial echo — just the one. assert sum(isinstance(e, StateSnapshotEvent) for e in events) == 1 @@ -370,7 +371,8 @@ async def collect(): _fake_stream("a"), _run_input(), emit_messages_snapshot=False, - emit_final_state=False, + initial_state={}, + final_state=None, ) ] @@ -380,7 +382,7 @@ async def collect(): assert snapshots[0].snapshot == {} -def test_to_agui_state_flags_false_suppress_both_snapshots(): +def test_to_agui_none_state_sources_suppress_both_snapshots(): translator = AGUITranslator(outbound_cls=_StubOutbound) run_input = _run_input().model_copy(update={"state": {"x": 1}}) @@ -391,8 +393,8 @@ async def collect(): _fake_stream("a"), run_input, emit_messages_snapshot=False, - emit_initial_state=False, - emit_final_state=False, + initial_state=None, + final_state=None, ) ] @@ -401,13 +403,21 @@ async def collect(): def test_to_agui_emits_final_state_before_messages_snapshot(): - # Default flags: state echoes twice — initial right after RUN_STARTED and - # final in the settled-state slot (#5), just before MESSAGES_SNAPSHOT. + # Pass both sources: state echoes twice — initial right after RUN_STARTED + # and final in the settled-state slot (#5), just before MESSAGES_SNAPSHOT. translator = AGUITranslator(outbound_cls=_StubOutbound) run_input = _run_input().model_copy(update={"state": {"theme": "dark"}}) async def collect(): - return [e async for e in translator.to_agui(_fake_stream("a"), run_input)] + return [ + e + async for e in translator.to_agui( + _fake_stream("a"), + run_input, + initial_state=run_input.state, + final_state=run_input.state, + ) + ] events = asyncio.run(collect()) snapshots = [e for e in events if isinstance(e, StateSnapshotEvent)] @@ -420,7 +430,55 @@ async def collect(): assert events[-3] is snapshots[-1] -def test_to_agui_emit_final_state_false_keeps_only_initial(): +def test_to_agui_accepts_static_and_lazy_state_sources(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + state = {"status": "starting"} + + async def collect(): + async def stream(): + state["status"] = "complete" + yield "a" + + return [ + e + async for e in translator.to_agui( + stream(), + _run_input(), + emit_messages_snapshot=False, + initial_state={"status": "starting"}, + final_state=lambda: dict(state), + ) + ] + + events = asyncio.run(collect()) + snapshots = [e.snapshot for e in events if isinstance(e, StateSnapshotEvent)] + assert snapshots == [{"status": "starting"}, {"status": "complete"}] + + +def test_to_agui_awaits_async_state_source(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + + async def final_state(): + return {"status": "complete"} + + async def collect(): + return [ + e + async for e in translator.to_agui( + _fake_stream("a"), + _run_input(), + emit_messages_snapshot=False, + initial_state=None, + final_state=final_state, + ) + ] + + events = asyncio.run(collect()) + snapshots = [e.snapshot for e in events if isinstance(e, StateSnapshotEvent)] + assert snapshots == [{"status": "complete"}] + + +def test_to_agui_final_state_none_keeps_only_initial(): translator = AGUITranslator(outbound_cls=_StubOutbound) run_input = _run_input().model_copy(update={"state": {"x": 1}}) @@ -431,7 +489,8 @@ async def collect(): _fake_stream("a"), run_input, emit_messages_snapshot=False, - emit_final_state=False, + initial_state=run_input.state, + final_state=None, ) ] From 700b08de2de972c21788969c098287760b0cca00 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 12 Jul 2026 23:04:13 +0200 Subject: [PATCH 29/94] feat(examples): remove handoff example with keeping translator logic --- apps/dojo/src/agents.ts | 1 - .../feature/(v2)/handoff/README.mdx | 26 ------ .../feature/(v2)/handoff/page.tsx | 51 ----------- .../feature/(v2)/subagents/README.mdx | 6 +- apps/dojo/src/config.ts | 7 -- apps/dojo/src/files.json | 30 ++----- apps/dojo/src/menu.ts | 1 - apps/dojo/src/types/integration.ts | 1 - integrations/openai-agents/python/README.md | 28 ++++-- .../openai-agents/python/examples/README.md | 26 ++---- .../examples/agents_examples/__init__.py | 3 - .../examples/agents_examples/agentic_chat.py | 2 +- .../examples/agents_examples/handoff.py | 90 ------------------- .../examples/agents_examples/subagents.py | 14 +-- .../openai-agents/python/examples/server.py | 1 - .../python/examples/translator_server.py | 1 - 16 files changed, 43 insertions(+), 245 deletions(-) delete mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/README.mdx delete mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/page.tsx delete mode 100644 integrations/openai-agents/python/examples/agents_examples/handoff.py diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 3fa71d4fe9..afa1d2e5dd 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -641,7 +641,6 @@ export const agentsIntegrations = { backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", tool_based_generative_ui: "tool_based_generative_ui", - handoff: "handoff", subagents: "subagents", custom_lifecycle_events: "custom_lifecycle_events", }, diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/README.mdx deleted file mode 100644 index 412abf37bb..0000000000 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/README.mdx +++ /dev/null @@ -1,26 +0,0 @@ -# 🤝 Handoff - -## What This Demo Shows - -Two handoff patterns from the SDK docs' `handoffs/` page, combined in one -triage agent: - -- passing an `Agent` directly (`billing_agent`) — plain handoff, no - customization -- wrapping with `handoff(agent=..., on_handoff=..., input_type=...)` - (`escalation_agent`) — the LLM fills in a structured `EscalationData` - reason, delivered to a callback before the specialist takes over - -## How to Interact - -- "I have a question about my invoice" (routes to billing_agent) -- "This isn't working and I've tried everything, I need to talk to someone" - (routes to escalation_agent with a captured reason) - -## Technical Details - -- The handoff shows up as an AG-UI tool call + result, same as any other tool -- `escalation_agent`'s structured input rides through as ordinary tool-call - JSON args — no special-casing in the translator -- Each hop emits `STEP_FINISHED` for the outgoing agent and `STEP_STARTED` - for the incoming one diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/page.tsx deleted file mode 100644 index c15b245c12..0000000000 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/handoff/page.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; -import React from "react"; -import "@copilotkit/react-core/v2/styles.css"; -import { CopilotChat, useConfigureSuggestions } from "@copilotkit/react-core/v2"; -import { CopilotKit } from "@copilotkit/react-core"; - -interface HandoffProps { - params: Promise<{ - integrationId: string; - }>; -} - -const Handoff: React.FC = ({ params }) => { - const { integrationId } = React.use(params); - - return ( - - - - ); -}; - -const Chat = () => { - useConfigureSuggestions({ - suggestions: [ - { - title: "Billing question", - message: "I have a question about my last invoice", - }, - { - title: "Escalate an issue", - message: "This isn't working and I've tried everything, I need to escalate this", - }, - ], - available: "always", - }); - - return ( -
-
- -
-
- ); -}; - -export default Handoff; diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx index 5917690701..79bea8bda3 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx @@ -5,9 +5,9 @@ A supervisor agent stays in charge of the conversation and calls specialist agents as tools (`Agent.as_tool()`), possibly several in one turn, then synthesizes their outputs itself — a call-and-return delegation, not a -handoff. Complements the `handoff` demo, where control transfers away from -the triage agent instead. Same shape as CopilotKit's own LangGraph -"subagents" showcase demo (a supervisor calling child agents as tools). +handoff (control never transfers away from the supervisor). Same shape as +CopilotKit's own LangGraph "subagents" showcase demo (a supervisor calling +child agents as tools). A sidebar tracks the team live: role chips light up while a specialist is working, and a delegation log lists each call with its status and a diff --git a/apps/dojo/src/config.ts b/apps/dojo/src/config.ts index 99ee02cb9f..b253e4ef83 100644 --- a/apps/dojo/src/config.ts +++ b/apps/dojo/src/config.ts @@ -88,13 +88,6 @@ export const featureConfig: FeatureConfig[] = [ "Have your tasks performed by multiple agents, working together", tags: ["Chat", "Multi-agent architecture", "Streaming", "Subgraphs"], }), - createFeatureConfig({ - id: "handoff", - name: "Handoff", - description: - "Triage agent hands the conversation off to a specialist agent", - tags: ["Multi-agent architecture", "Agentic architecture", "Handoff", "Delegation"], - }), createFeatureConfig({ id: "subagents", name: "Sub-agents", diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 515b663232..a8ec93d026 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4828,7 +4828,7 @@ }, { "name": "agentic_chat.py", - "content": "\"\"\"Agentic chat — plain conversation, no tools.\n\nThe baseline demo: exercises ``TEXT_MESSAGE_START/CONTENT/END`` only. Good\nfirst smoke test before trying the tool / handoff examples.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_agentic_chat_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n", + "content": "\"\"\"Agentic chat — plain conversation, no tools.\n\nThe baseline demo: exercises ``TEXT_MESSAGE_START/CONTENT/END`` only. Good\nfirst smoke test before trying the tool / multi-agent examples.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_agentic_chat_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n", "language": "python", "type": "file" } @@ -4905,26 +4905,6 @@ "type": "file" } ], - "openai-agents-python::handoff": [ - { - "name": "page.tsx", - "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useConfigureSuggestions } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface HandoffProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst Handoff: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Billing question\",\n message: \"I have a question about my last invoice\",\n },\n {\n title: \"Escalate an issue\",\n message: \"This isn't working and I've tried everything, I need to escalate this\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default Handoff;\n", - "language": "typescript", - "type": "file" - }, - { - "name": "README.mdx", - "content": "# 🤝 Handoff\n\n## What This Demo Shows\n\nTwo handoff patterns from the SDK docs' `handoffs/` page, combined in one\ntriage agent:\n\n- passing an `Agent` directly (`billing_agent`) — plain handoff, no\n customization\n- wrapping with `handoff(agent=..., on_handoff=..., input_type=...)`\n (`escalation_agent`) — the LLM fills in a structured `EscalationData`\n reason, delivered to a callback before the specialist takes over\n\n## How to Interact\n\n- \"I have a question about my invoice\" (routes to billing_agent)\n- \"This isn't working and I've tried everything, I need to talk to someone\"\n (routes to escalation_agent with a captured reason)\n\n## Technical Details\n\n- The handoff shows up as an AG-UI tool call + result, same as any other tool\n- `escalation_agent`'s structured input rides through as ordinary tool-call\n JSON args — no special-casing in the translator\n- Each hop emits `STEP_FINISHED` for the outgoing agent and `STEP_STARTED`\n for the incoming one\n", - "language": "markdown", - "type": "file" - }, - { - "name": "handoff.py", - "content": "\"\"\"Handoff — the SDK docs' own patterns from ``handoffs/``, wired for AG-UI.\n\nCombines the two handoff shapes shown on that page in one triage agent:\n\n* ``handoffs=[billing_agent]`` — passing an ``Agent`` directly (the\n \"Basic usage\" section). No customization; the SDK derives the\n ``transfer_to_billing_agent`` tool itself.\n* ``handoffs=[handoff(agent=..., on_handoff=..., input_type=...)]`` — the\n \"Handoff inputs\" section: a Pydantic model the LLM fills in when it\n triggers the handoff, delivered to ``on_handoff`` before the specialist\n ever sees the conversation. Used here for escalation_agent so the reason\n is captured structurally instead of buried in free text.\n\nAlso uses ``RECOMMENDED_PROMPT_PREFIX`` (the \"Recommended prompts\" section)\non every agent that receives handoffs, since the docs call out that models\nhandle multi-agent handoffs more reliably with it in the instructions.\n\nExercises:\n\n* ``translate_handoff_call_item`` / ``translate_handoff_output_item`` — the\n handoff shows up as an AG-UI tool call + result, same as any other tool.\n ``escalation_agent``'s structured input rides through as ordinary\n ``TOOL_CALL_ARGS`` JSON — no special-casing needed, since the translator\n dispatches on run-item type, not on whether the tool happens to be a\n handoff.\n* ``translate_agent_updated_event`` — each hop emits ``STEP_FINISHED`` for\n the outgoing agent and ``STEP_STARTED`` for the incoming one, so the\n client can label which agent produced which message.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, RunContextWrapper, handoff\nfrom agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX\nfrom pydantic import BaseModel\n\nfrom .constants import DEFAULT_MODEL\n\nbilling_agent = Agent(\n name=\"billing_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n f\"{RECOMMENDED_PROMPT_PREFIX}\\n\"\n \"You handle billing questions: invoices, charges, payment methods. \"\n \"Be precise and concise.\"\n ),\n)\n\nescalation_agent = Agent(\n name=\"escalation_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n f\"{RECOMMENDED_PROMPT_PREFIX}\\n\"\n \"You handle escalated issues that the triage agent couldn't resolve. \"\n \"Acknowledge the reason you were given and ask what outcome the \"\n \"customer is looking for.\"\n ),\n)\n\n\nclass EscalationData(BaseModel):\n \"\"\"Structured input the triage agent fills in when escalating.\"\"\"\n\n reason: str\n\n\nasync def on_escalation_handoff(ctx: RunContextWrapper[None], input_data: EscalationData) -> None:\n print(f\"Escalation agent called with reason: {input_data.reason}\")\n\n\ndef create_handoff_agent() -> Agent:\n return Agent(\n name=\"triage_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n f\"{RECOMMENDED_PROMPT_PREFIX}\\n\"\n \"You triage customer support requests. Hand off to billing_agent \"\n \"for billing questions. For anything you can't resolve yourself, \"\n \"hand off to escalation_agent with a short reason. Handle \"\n \"anything else yourself.\"\n ),\n handoffs=[\n billing_agent,\n handoff(\n agent=escalation_agent,\n on_handoff=on_escalation_handoff,\n input_type=EscalationData,\n ),\n ],\n )\n", - "language": "python", - "type": "file" - } - ], "openai-agents-python::subagents": [ { "name": "page.tsx", @@ -4934,13 +4914,13 @@ }, { "name": "README.mdx", - "content": "# 🧭 Subagents\n\n## What This Demo Shows\n\nA supervisor agent stays in charge of the conversation and calls specialist\nagents as tools (`Agent.as_tool()`), possibly several in one turn, then\nsynthesizes their outputs itself — a call-and-return delegation, not a\nhandoff. Complements the `handoff` demo, where control transfers away from\nthe triage agent instead. Same shape as CopilotKit's own LangGraph\n\"subagents\" showcase demo (a supervisor calling child agents as tools).\n\nA sidebar tracks the team live: role chips light up while a specialist is\nworking, and a delegation log lists each call with its status and a\npreview of the input/output.\n\n## How to Interact\n\n- \"Write a short piece about the history of coffee\" (calls research,\n writer, and critic specialists in sequence, then produces a final draft)\n- \"What is 2 + 2?\" (answered directly, no team involved)\n\n## Technical Details\n\n- Each specialist invocation is an ordinary `TOOL_CALL_START/ARGS/END` +\n `TOOL_CALL_RESULT` sequence\n- The nested agents' own model turns stay internal to the SDK and are not\n streamed as separate messages — the client sees one coherent transcript\n- The sidebar is built from three `useRenderTool` renderers (one per\n specialist tool name), each reporting its status into shared page state\n via a small tracker component\n", + "content": "# 🧭 Subagents\n\n## What This Demo Shows\n\nA supervisor agent stays in charge of the conversation and calls specialist\nagents as tools (`Agent.as_tool()`), possibly several in one turn, then\nsynthesizes their outputs itself — a call-and-return delegation, not a\nhandoff (control never transfers away from the supervisor). Same shape as\nCopilotKit's own LangGraph \"subagents\" showcase demo (a supervisor calling\nchild agents as tools).\n\nA sidebar tracks the team live: role chips light up while a specialist is\nworking, and a delegation log lists each call with its status and a\npreview of the input/output.\n\n## How to Interact\n\n- \"Write a short piece about the history of coffee\" (calls research,\n writer, and critic specialists in sequence, then produces a final draft)\n- \"What is 2 + 2?\" (answered directly, no team involved)\n\n## Technical Details\n\n- Each specialist invocation is an ordinary `TOOL_CALL_START/ARGS/END` +\n `TOOL_CALL_RESULT` sequence\n- The nested agents' own model turns stay internal to the SDK and are not\n streamed as separate messages — the client sees one coherent transcript\n- The sidebar is built from three `useRenderTool` renderers (one per\n specialist tool name), each reporting its status into shared page state\n via a small tracker component\n", "language": "markdown", "type": "file" }, { "name": "subagents.py", - "content": "\"\"\"Subagents — multi-agent via the SDK's agents-as-tools pattern.\n\nComplements :mod:`handoff`: there, control *transfers* to the specialist\n(triage agent exits the conversation). Here the supervisor stays in charge\nand *calls* specialists as tools (``Agent.as_tool()``), possibly several in\none turn, then synthesizes their outputs itself — a call-and-return\ndelegation, not a handoff. Same shape as the LangGraph \"subagents\" showcase\ndemo (a supervisor calling child agents as tools and getting results back),\njust built with the SDK's own ``as_tool()`` instead of a routing graph.\n\nOn the AG-UI side each specialist invocation is an ordinary\n``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested\nagent's own model turns stay internal to the SDK and are not streamed as\nseparate messages, so the client sees one coherent supervisor transcript.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\nresearch_agent = Agent(\n name=\"research_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You research a topic and return the key facts as a short bullet \"\n \"list. Facts only — no fluff, no conclusions.\"\n ),\n)\n\nwriter_agent = Agent(\n name=\"writer_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You turn bullet-point facts into short, engaging prose. One tight \"\n \"paragraph unless asked otherwise.\"\n ),\n)\n\ncritic_agent = Agent(\n name=\"critic_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You review a draft and return concrete improvement suggestions as a \"\n \"numbered list. Max 3 suggestions, be specific.\"\n ),\n)\n\n\ndef create_subagents_agent() -> Agent:\n return Agent(\n name=\"orchestrator\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You orchestrate a small content team. For writing requests: \"\n \"call research_topic for the facts, then write_prose to draft, \"\n \"then critique_draft to review, then produce the final version \"\n \"yourself incorporating the critique. For simple questions, \"\n \"answer directly without the team.\"\n ),\n tools=[\n research_agent.as_tool(\n tool_name=\"research_topic\",\n tool_description=\"Research a topic and return key facts as bullets.\",\n ),\n writer_agent.as_tool(\n tool_name=\"write_prose\",\n tool_description=\"Turn bullet-point facts into short prose.\",\n ),\n critic_agent.as_tool(\n tool_name=\"critique_draft\",\n tool_description=\"Review a draft and suggest up to 3 improvements.\",\n ),\n ],\n )\n", + "content": "\"\"\"Subagents — multi-agent via the SDK's agents-as-tools pattern.\n\nUnlike a handoff (control *transfers* to the specialist, triage agent exits\nthe conversation), the supervisor here stays in charge and *calls*\nspecialists as tools (``Agent.as_tool()``), possibly several in one turn,\nthen synthesizes their outputs itself — a call-and-return delegation, not a\nhandoff. Same shape as the LangGraph \"subagents\" showcase demo (a supervisor\ncalling child agents as tools and getting results back), just built with the\nSDK's own ``as_tool()`` instead of a routing graph.\n\nOn the AG-UI side each specialist invocation is an ordinary\n``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested\nagent's own model turns stay internal to the SDK and are not streamed as\nseparate messages, so the client sees one coherent supervisor transcript.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\nresearch_agent = Agent(\n name=\"research_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You research a topic and return the key facts as a short bullet \"\n \"list. Facts only — no fluff, no conclusions.\"\n ),\n)\n\nwriter_agent = Agent(\n name=\"writer_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You turn bullet-point facts into short, engaging prose. One tight \"\n \"paragraph unless asked otherwise.\"\n ),\n)\n\ncritic_agent = Agent(\n name=\"critic_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You review a draft and return concrete improvement suggestions as a \"\n \"numbered list. Max 3 suggestions, be specific.\"\n ),\n)\n\n\ndef create_subagents_agent() -> Agent:\n return Agent(\n name=\"orchestrator\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You orchestrate a small content team. For writing requests: \"\n \"call research_topic for the facts, then write_prose to draft, \"\n \"then critique_draft to review, then produce the final version \"\n \"yourself incorporating the critique. For simple questions, \"\n \"answer directly without the team.\"\n ),\n tools=[\n research_agent.as_tool(\n tool_name=\"research_topic\",\n tool_description=\"Research a topic and return key facts as bullets.\",\n ),\n writer_agent.as_tool(\n tool_name=\"write_prose\",\n tool_description=\"Turn bullet-point facts into short prose.\",\n ),\n critic_agent.as_tool(\n tool_name=\"critique_draft\",\n tool_description=\"Review a draft and suggest up to 3 improvements.\",\n ),\n ],\n )\n", "language": "python", "type": "file" } @@ -4948,13 +4928,13 @@ "openai-agents-python::custom_lifecycle_events": [ { "name": "page.tsx", - "content": "\"use client\";\n\nimport React, {\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface UsageInfo {\n tokens: number;\n costUsd: number;\n}\n\ninterface RunUsage {\n input?: UsageInfo;\n output?: UsageInfo;\n}\n\nconst runUsage = new Map();\nconst usageListeners = new Set<() => void>();\n\nfunction setRunUsage(runId: string, update: RunUsage) {\n runUsage.set(runId, { ...runUsage.get(runId), ...update });\n usageListeners.forEach((listener) => listener());\n}\n\nfunction subscribeRunUsage(listener: () => void) {\n usageListeners.add(listener);\n return () => usageListeners.delete(listener);\n}\n\nfunction getRunUsage(runId?: string) {\n return runId ? runUsage.get(runId) : undefined;\n}\n\nfunction UsageLabel({ runId }: { runId?: string }) {\n const usage = useSyncExternalStore(\n subscribeRunUsage,\n () => getRunUsage(runId),\n () => undefined,\n );\n\n if (!usage?.input && !usage?.output) {\n return null;\n }\n\n return (\n \n {usage.input && (\n \n ⬇️ {usage.input.tokens} tok · ${usage.input.costUsd.toFixed(6)}\n \n )}\n {usage.input && usage.output && }\n {usage.output && (\n \n ⬆️ {usage.output.tokens} tok · ${usage.output.costUsd.toFixed(6)}\n \n )}\n
\n );\n}\n\nconst CustomLifecycleEvents: React.FC = ({\n params,\n}) => {\n const { integrationId } = React.use(params);\n const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const currentRunId = useRef(undefined);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n {\n title: \"Tell a fact\",\n message: \"Tell me one interesting fact about space.\",\n },\n ],\n available: \"always\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onRunStartedEvent: ({ event }) => {\n currentRunId.current = event.runId;\n runUsage.delete(event.runId);\n usageListeners.forEach((listener) => listener());\n },\n onCustomEvent: ({ event }) => {\n const runId = currentRunId.current;\n if (!runId) return;\n\n const value = event.value as { tokens: number; cost_usd: number };\n if (event.name === \"input_usage\") {\n setRunUsage(runId, {\n input: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n if (event.name === \"output_usage\") {\n setRunUsage(runId, {\n output: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n },\n onRunFinishedEvent: () => {\n currentRunId.current = undefined;\n },\n onRunErrorEvent: () => {\n currentRunId.current = undefined;\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default CustomLifecycleEvents;\n", + "content": "\"use client\";\n\nimport React, {\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface UsageInfo {\n tokens: number;\n costUsd: number;\n}\n\ninterface RunUsage {\n input?: UsageInfo;\n output?: UsageInfo;\n}\n\nconst runUsage = new Map();\nconst usageListeners = new Set<() => void>();\n\nfunction setRunUsage(runId: string, update: RunUsage) {\n runUsage.set(runId, { ...runUsage.get(runId), ...update });\n usageListeners.forEach((listener) => listener());\n}\n\nfunction subscribeRunUsage(listener: () => void) {\n usageListeners.add(listener);\n return () => usageListeners.delete(listener);\n}\n\nfunction getRunUsage(runId?: string) {\n return runId ? runUsage.get(runId) : undefined;\n}\n\nfunction UsageLabel({\n message,\n position,\n runId,\n}: {\n message: { role: string };\n position: \"before\" | \"after\";\n runId?: string;\n}) {\n const usage = useSyncExternalStore(\n subscribeRunUsage,\n () => getRunUsage(runId),\n () => undefined,\n );\n\n if (\n message.role !== \"assistant\" ||\n position !== \"after\" ||\n (!usage?.input && !usage?.output)\n ) {\n return null;\n }\n\n return (\n \n {usage.input && (\n \n ⬇️ {usage.input.tokens} tokens · ${usage.input.costUsd.toFixed(6)}\n \n )}\n {usage.input && usage.output && }\n {usage.output && (\n \n ⬆️ {usage.output.tokens} tokens · ${usage.output.costUsd.toFixed(6)}\n \n )}\n
\n );\n}\n\nconst CustomLifecycleEvents: React.FC = ({\n params,\n}) => {\n const { integrationId } = React.use(params);\n const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const currentRunId = useRef(undefined);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n {\n title: \"Tell me a fact about AI\",\n message: \"Tell me one interesting fact about AI.\",\n },\n ],\n available: \"before-first-message\",\n consumerAgentId: \"custom_lifecycle_events\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onRunStartedEvent: ({ event }) => {\n currentRunId.current = event.runId;\n runUsage.delete(event.runId);\n usageListeners.forEach((listener) => listener());\n },\n onCustomEvent: ({ event }) => {\n const runId = currentRunId.current;\n if (!runId) return;\n\n const value = event.value as { tokens: number; cost_usd: number };\n if (event.name === \"input_usage\") {\n setRunUsage(runId, {\n input: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n if (event.name === \"output_usage\") {\n setRunUsage(runId, {\n output: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n },\n onRunFinishedEvent: () => {\n currentRunId.current = undefined;\n },\n onRunErrorEvent: () => {\n currentRunId.current = undefined;\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default CustomLifecycleEvents;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes two optional params — `start_custom_event`\nand `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance\n(anything else raises `TypeError`), default `None` (off). When set, one\n`CUSTOM` event is emitted right after `RUN_STARTED`, another right before\n`RUN_FINISHED`. This demo's server (`translator_server.py`) uses them to\nreport fake usage split the way a real bill would be: `input_usage`\n(prompt tokens/cost — known before the model even runs) at the start,\n`output_usage` (completion tokens/cost — known only once the run is done)\nat the end — plain conversation otherwise, same agent as `agentic_chat`.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about space.\"\n\nEach assistant reply has a compact usage label immediately above it. The label\nshows the input usage reported at the beginning of the run and the output usage\nreported at the end.\n\n## Technical Details\n\n- `build_input_usage_event()`/`build_output_usage_event()`\n (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s;\n `translator_server.py`'s route just calls them and passes the result to\n `to_agui(start_custom_event=..., end_custom_event=...)`\n- Both events are opaque to the translator — it only checks `isinstance(...,\n CustomEvent)`, the `name`/`value` shape is entirely up to the caller\n- The page captures the current `runId` from `RUN_STARTED`, associates the\n custom usage events with that run in a small external store, then uses\n `CopilotKitProvider`'s stable `renderCustomMessages` configuration to render\n the matching label above the assistant message\n- The token/cost numbers are fake — the point is the event bracketing, not\n real usage accounting\n", + "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes two optional params — `start_custom_event`\nand `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance\n(anything else raises `TypeError`), default `None` (off). When set, one\n`CUSTOM` event is emitted right after `RUN_STARTED`, another right before\n`RUN_FINISHED`. This demo's server (`translator_server.py`) uses them to\nreport fake usage split the way a real bill would be: `input_usage`\n(prompt tokens/cost — known before the model even runs) at the start,\n`output_usage` (completion tokens/cost — known only once the run is done)\nat the end — plain conversation otherwise, same agent as `agentic_chat`.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about AI.\"\n\nEach assistant reply has a compact usage label immediately above it. The label\nshows the input usage reported at the beginning of the run and the output usage\nreported at the end.\n\n## Technical Details\n\n- `build_input_usage_event()`/`build_output_usage_event()`\n (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s;\n `translator_server.py`'s route just calls them and passes the result to\n `to_agui(start_custom_event=..., end_custom_event=...)`\n- Both events are opaque to the translator — it only checks `isinstance(...,\n CustomEvent)`, the `name`/`value` shape is entirely up to the caller\n- The page captures the current `runId` from `RUN_STARTED`, associates the\n custom usage events with that run in a small external store, then uses\n `CopilotKitProvider`'s stable `renderCustomMessages` configuration to render\n the matching label above the assistant message\n- The token/cost numbers are fake — the point is the event bracketing, not\n real usage accounting\n", "language": "markdown", "type": "file" }, diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index 1588cbc26f..f1b68bd587 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -385,7 +385,6 @@ export const menuIntegrations = [ "backend_tool_rendering", "human_in_the_loop", "tool_based_generative_ui", - "handoff", "subagents", "custom_lifecycle_events", ], diff --git a/apps/dojo/src/types/integration.ts b/apps/dojo/src/types/integration.ts index 5f1294f7a6..241d8c1c22 100644 --- a/apps/dojo/src/types/integration.ts +++ b/apps/dojo/src/types/integration.ts @@ -20,7 +20,6 @@ export type Feature = | "a2ui_advanced" | "a2ui_recovery" | "crew_chat" - | "handoff" | "subagents" | "custom_lifecycle_events" | "error_flow" diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 6e54d40a35..2c598b46d9 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -108,14 +108,26 @@ Expected: `RUN_STARTED -> STATE_SNAPSHOT -> TEXT_MESSAGE_START -> TEXT_MESSAGE_CONTENT (xN) -> TEXT_MESSAGE_END -> STATE_SNAPSHOT -> MESSAGES_SNAPSHOT -> RUN_FINISHED`. -`run_input.state` is echoed as a `STATE_SNAPSHOT` twice — once right after -`RUN_STARTED` (`emit_initial_state`) and once just before `MESSAGES_SNAPSHOT` -(`emit_final_state`) — both defaulting on, both gated only on -`state is not None` (an empty `{}` still emits; only `null` is skipped). The -OpenAI Agents SDK has no shared-state channel, so nothing mutates state -mid-run; the final echo is the settled-state slot the frontend can rely on -ending with. Pass `emit_initial_state=False` / `emit_final_state=False` to -`to_agui` to suppress either. +`initial_state` and `final_state` control the two `STATE_SNAPSHOT` slots. +Omit them to echo `run_input.state` as before, pass `None` to suppress a slot, +or pass a static value, zero-argument function, or zero-argument async +function. The initial source is resolved right after `RUN_STARTED`; the final +source is resolved after successful streaming, just before +`MESSAGES_SNAPSHOT`. This lets hooks and tools update application-owned state: + +```python +state = dict(run_input.state or {}) +initial_snapshot = dict(state) + +result = Runner.run_streamed(agent, input=bundle.messages, context=state) +async for event in translator.to_agui( + result, + run_input, + initial_state=initial_snapshot, + final_state=lambda: dict(state), +): + ... +``` A full multi-demo server (chat, backend tools, human-in-the-loop, handoffs, orchestrator) lives in [`examples/`](examples/). diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 8c4d5ac0d5..7823b974d7 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -48,8 +48,8 @@ curl -N -X POST http://localhost:8022/agentic_chat \ ``` Swap the path to hit a different demo — `GET /health` lists every registered -agent. Demos map 1:1 onto the AG-UI Dojo feature pages, plus two multi-agent -patterns (`handoff`, `orchestrator`). +agent. Demos map 1:1 onto the AG-UI Dojo feature pages, plus a multi-agent +pattern (`orchestrator`). > The stateful demos (`shared_state`, `agentic_generative_ui`, > `predictive_state_updates`) are shelved together with the `AGUIContext` @@ -90,24 +90,12 @@ mechanics as `human_in_the_loop`. **Try:** `"Write me a haiku about the ocean."` — needs a client that sends a `generate_haiku` tool definition (Dojo's tool-based generative UI page). -### `handoff` -Multi-agent triage using the SDK's native `handoffs=`. A `triage_agent` -hands off to `billing_agent` or `refund_agent` depending on the request. -Exercises handoff call/result translation (`translate_handoff_call_item` / -`translate_handoff_output_item`) and per-agent `STEP_STARTED`/`STEP_FINISHED` -pairs (`translate_agent_updated_event`) so the client can attribute each -message to the agent that produced it. - -**Try:** `"I was charged twice for my last invoice."` (routes to -`billing_agent`) or `"I want a refund for order #1234."` (routes to -`refund_agent`). - ### `orchestrator` Multi-agent via the SDK's **agents-as-tools** pattern (`Agent.as_tool()`) — -the complement to `handoff`: control never transfers, the orchestrator calls -`research_agent`, `writer_agent`, and `critic_agent` as tools and -synthesizes the final answer itself. Each specialist invocation appears to -the client as a normal `TOOL_CALL_*` + `TOOL_CALL_RESULT` sequence; the -nested agents' inner turns stay internal to the SDK. +control never transfers, the orchestrator calls `research_agent`, +`writer_agent`, and `critic_agent` as tools and synthesizes the final answer +itself. Each specialist invocation appears to the client as a normal +`TOOL_CALL_*` + `TOOL_CALL_RESULT` sequence; the nested agents' inner turns +stay internal to the SDK. **Try:** `"Write a short piece about the history of coffee."` diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py index 72415130a9..a131303036 100644 --- a/integrations/openai-agents/python/examples/agents_examples/__init__.py +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -31,7 +31,6 @@ build_output_usage_event, create_custom_lifecycle_events_agent, ) -from .handoff import create_handoff_agent from .human_in_the_loop import create_human_in_the_loop_agent from .subagents import create_subagents_agent from .tool_based_generative_ui import create_tool_based_generative_ui_agent @@ -53,7 +52,6 @@ def build_registry() -> dict[str, DemoConfig]: "backend_tool_rendering": DemoConfig(agent=create_backend_tool_agent()), "human_in_the_loop": DemoConfig(agent=create_human_in_the_loop_agent()), "tool_based_generative_ui": DemoConfig(agent=create_tool_based_generative_ui_agent()), - "handoff": DemoConfig(agent=create_handoff_agent()), "subagents": DemoConfig(agent=create_subagents_agent()), "custom_lifecycle_events": DemoConfig( agent=create_custom_lifecycle_events_agent(), @@ -69,7 +67,6 @@ def build_registry() -> dict[str, DemoConfig]: "create_agentic_chat_agent", "create_backend_tool_agent", "create_custom_lifecycle_events_agent", - "create_handoff_agent", "create_human_in_the_loop_agent", "create_subagents_agent", "create_tool_based_generative_ui_agent", diff --git a/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py b/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py index 25cfeb6bac..7e783ddefd 100644 --- a/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py +++ b/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py @@ -1,7 +1,7 @@ """Agentic chat — plain conversation, no tools. The baseline demo: exercises ``TEXT_MESSAGE_START/CONTENT/END`` only. Good -first smoke test before trying the tool / handoff examples. +first smoke test before trying the tool / multi-agent examples. """ from __future__ import annotations diff --git a/integrations/openai-agents/python/examples/agents_examples/handoff.py b/integrations/openai-agents/python/examples/agents_examples/handoff.py deleted file mode 100644 index 84bf4d2182..0000000000 --- a/integrations/openai-agents/python/examples/agents_examples/handoff.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Handoff — the SDK docs' own patterns from ``handoffs/``, wired for AG-UI. - -Combines the two handoff shapes shown on that page in one triage agent: - -* ``handoffs=[billing_agent]`` — passing an ``Agent`` directly (the - "Basic usage" section). No customization; the SDK derives the - ``transfer_to_billing_agent`` tool itself. -* ``handoffs=[handoff(agent=..., on_handoff=..., input_type=...)]`` — the - "Handoff inputs" section: a Pydantic model the LLM fills in when it - triggers the handoff, delivered to ``on_handoff`` before the specialist - ever sees the conversation. Used here for escalation_agent so the reason - is captured structurally instead of buried in free text. - -Also uses ``RECOMMENDED_PROMPT_PREFIX`` (the "Recommended prompts" section) -on every agent that receives handoffs, since the docs call out that models -handle multi-agent handoffs more reliably with it in the instructions. - -Exercises: - -* ``translate_handoff_call_item`` / ``translate_handoff_output_item`` — the - handoff shows up as an AG-UI tool call + result, same as any other tool. - ``escalation_agent``'s structured input rides through as ordinary - ``TOOL_CALL_ARGS`` JSON — no special-casing needed, since the translator - dispatches on run-item type, not on whether the tool happens to be a - handoff. -* ``translate_agent_updated_event`` — each hop emits ``STEP_FINISHED`` for - the outgoing agent and ``STEP_STARTED`` for the incoming one, so the - client can label which agent produced which message. -""" - -from __future__ import annotations - -from agents import Agent, RunContextWrapper, handoff -from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX -from pydantic import BaseModel - -from .constants import DEFAULT_MODEL - -billing_agent = Agent( - name="billing_agent", - model=DEFAULT_MODEL, - instructions=( - f"{RECOMMENDED_PROMPT_PREFIX}\n" - "You handle billing questions: invoices, charges, payment methods. " - "Be precise and concise." - ), -) - -escalation_agent = Agent( - name="escalation_agent", - model=DEFAULT_MODEL, - instructions=( - f"{RECOMMENDED_PROMPT_PREFIX}\n" - "You handle escalated issues that the triage agent couldn't resolve. " - "Acknowledge the reason you were given and ask what outcome the " - "customer is looking for." - ), -) - - -class EscalationData(BaseModel): - """Structured input the triage agent fills in when escalating.""" - - reason: str - - -async def on_escalation_handoff(ctx: RunContextWrapper[None], input_data: EscalationData) -> None: - print(f"Escalation agent called with reason: {input_data.reason}") - - -def create_handoff_agent() -> Agent: - return Agent( - name="triage_agent", - model=DEFAULT_MODEL, - instructions=( - f"{RECOMMENDED_PROMPT_PREFIX}\n" - "You triage customer support requests. Hand off to billing_agent " - "for billing questions. For anything you can't resolve yourself, " - "hand off to escalation_agent with a short reason. Handle " - "anything else yourself." - ), - handoffs=[ - billing_agent, - handoff( - agent=escalation_agent, - on_handoff=on_escalation_handoff, - input_type=EscalationData, - ), - ], - ) diff --git a/integrations/openai-agents/python/examples/agents_examples/subagents.py b/integrations/openai-agents/python/examples/agents_examples/subagents.py index 8deb96a336..53af3fb6cd 100644 --- a/integrations/openai-agents/python/examples/agents_examples/subagents.py +++ b/integrations/openai-agents/python/examples/agents_examples/subagents.py @@ -1,12 +1,12 @@ """Subagents — multi-agent via the SDK's agents-as-tools pattern. -Complements :mod:`handoff`: there, control *transfers* to the specialist -(triage agent exits the conversation). Here the supervisor stays in charge -and *calls* specialists as tools (``Agent.as_tool()``), possibly several in -one turn, then synthesizes their outputs itself — a call-and-return -delegation, not a handoff. Same shape as the LangGraph "subagents" showcase -demo (a supervisor calling child agents as tools and getting results back), -just built with the SDK's own ``as_tool()`` instead of a routing graph. +Unlike a handoff (control *transfers* to the specialist, triage agent exits +the conversation), the supervisor here stays in charge and *calls* +specialists as tools (``Agent.as_tool()``), possibly several in one turn, +then synthesizes their outputs itself — a call-and-return delegation, not a +handoff. Same shape as the LangGraph "subagents" showcase demo (a supervisor +calling child agents as tools and getting results back), just built with the +SDK's own ``as_tool()`` instead of a routing graph. On the AG-UI side each specialist invocation is an ordinary ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested diff --git a/integrations/openai-agents/python/examples/server.py b/integrations/openai-agents/python/examples/server.py index 48f66b6d3f..b4904e84a4 100644 --- a/integrations/openai-agents/python/examples/server.py +++ b/integrations/openai-agents/python/examples/server.py @@ -15,7 +15,6 @@ POST /backend_tool_rendering ← server-executed @function_tool POST /human_in_the_loop ← frontend-owned tool, StopAtTools POST /tool_based_generative_ui ← frontend tool renders the content - POST /handoff ← multi-agent triage via handoffs= POST /orchestrator ← multi-agent via agents-as-tools GET /health ← liveness check (lists all demos) diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index a26e9db468..39d77f5d35 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -14,7 +14,6 @@ POST /backend_tool_rendering ← server-executed @function_tool POST /human_in_the_loop ← frontend-owned tool, StopAtTools POST /tool_based_generative_ui ← frontend tool renders the content - POST /handoff ← multi-agent triage via handoffs= POST /orchestrator ← multi-agent via agents-as-tools POST /custom_lifecycle_events ← manual CUSTOM event right after RUN_STARTED and right before RUN_FINISHED, via From 6ba6e9bda40274bf71a232986bc81d1fe7db2bcf Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 12 Jul 2026 23:50:39 +0200 Subject: [PATCH 30/94] feat(examples): add dynamic-system-prompt to inject user context example --- apps/dojo/src/agents.ts | 1 + .../(v2)/dynamic_system_prompt/README.mdx | 37 +++++++ .../(v2)/dynamic_system_prompt/page.tsx | 85 ++++++++++++++ apps/dojo/src/config.ts | 7 ++ apps/dojo/src/files.json | 20 ++++ apps/dojo/src/menu.ts | 1 + apps/dojo/src/types/integration.ts | 1 + .../examples/agents_examples/__init__.py | 15 +++ .../agents_examples/dynamic_system_prompt.py | 104 ++++++++++++++++++ .../openai-agents/python/examples/server.py | 67 ++++++++++- 10 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/page.tsx create mode 100644 integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index afa1d2e5dd..8217c7cfaa 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -643,6 +643,7 @@ export const agentsIntegrations = { tool_based_generative_ui: "tool_based_generative_ui", subagents: "subagents", custom_lifecycle_events: "custom_lifecycle_events", + dynamic_system_prompt: "dynamic_system_prompt", }, ), diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx new file mode 100644 index 0000000000..a8dd6091aa --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx @@ -0,0 +1,37 @@ +# 🌐 Dynamic System Prompt + +## What This Demo Shows + +The frontend sends a language choice over the AG-UI **context** channel — a +plain `{description, value}` pair, no special class involved. The agent's +`instructions` is a callable (`Agent.instructions`, the OpenAI Agents SDK's +own dynamic-instructions feature) that reads the context list every turn and +bakes "always reply in X" into the system prompt fresh, before each model +call. + +Context needs no `AGUIContext`/state machinery — it's read-only and +regenerated every run, so this demo drives the translator directly instead +of the shared wrapper: `to_sdk()` / `Runner.run_streamed(..., context=...)` +/ `to_agui()`, the same three calls as the other servers, plus the one line +they skip — passing the AG-UI context straight through as the SDK's own +`context=`. + +## How to Interact + +- Pick a language in the sidebar, then ask anything — the reply comes back + entirely in that language, regardless of what language you type in. +- Switch languages mid-conversation and ask again — the next reply follows + the new choice immediately, no restart needed. + +## Technical Details + +- `useAgentContext({ description: "Reply language", value: language })` — + pushes the current pick onto `RunAgentInput.context` with every message +- `dynamic_instructions(ctx, agent)` in + `examples/agents_examples/dynamic_system_prompt.py` reads + `ctx.context` (the raw `list[Context]` sent by the client) and returns a + fresh instructions string +- Runs as its own standalone FastAPI app (own file, own port) rather than + through the shared demo registry — the shared servers don't forward + `RunAgentInput.context` into `Runner.run_streamed`, so a demo that needs + it runs independently instead of changing that shared loop diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/page.tsx new file mode 100644 index 0000000000..f8673be2c3 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/page.tsx @@ -0,0 +1,85 @@ +"use client"; +import React, { useState } from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { CopilotChat, useAgentContext } from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; + +interface DynamicSystemPromptProps { + params: Promise<{ + integrationId: string; + }>; +} + +type Language = "English" | "Arabic" | "German"; + +const LANGUAGES: { value: Language; label: string; flag: string }[] = [ + { value: "English", label: "English", flag: "🇬🇧" }, + { value: "Arabic", label: "Arabic", flag: "🇸🇦" }, + { value: "German", label: "German", flag: "🇩🇪" }, +]; + +const DynamicSystemPrompt: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + ); +}; + +const DynamicSystemPromptView = () => { + const [language, setLanguage] = useState("English"); + + // The only thing this demo sends: one context item the agent's + // instructions callable reads on every turn to pick the reply language. + // No tool, no state — just the AG-UI context channel. + useAgentContext({ + description: "Reply language", + value: language, + }); + + return ( +
+
+

Reply language

+
+ {LANGUAGES.map(({ value, label, flag }) => ( + + ))} +
+

+ Switching language updates the AG-UI context sent with the next + message — the agent's system prompt is rebuilt every turn to + match. +

+
+ +
+
+ +
+
+
+ ); +}; + +export default DynamicSystemPrompt; diff --git a/apps/dojo/src/config.ts b/apps/dojo/src/config.ts index b253e4ef83..ede7442b97 100644 --- a/apps/dojo/src/config.ts +++ b/apps/dojo/src/config.ts @@ -102,6 +102,13 @@ export const featureConfig: FeatureConfig[] = [ "Manual CUSTOM events bracket the run — a session marker right after start, a usage summary right before finish", tags: ["Custom Events", "Lifecycle", "Translator"], }), + createFeatureConfig({ + id: "dynamic_system_prompt", + name: "Dynamic System Prompt", + description: + "Reply language picked from the AG-UI context channel, baked fresh into the system prompt every turn", + tags: ["Context", "Dynamic System Prompt"], + }), createFeatureConfig({ id: "a2a_chat", name: "A2A Chat", diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index a8ec93d026..bf23bb01be 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4945,6 +4945,26 @@ "type": "file" } ], + "openai-agents-python::dynamic_system_prompt": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useAgentContext } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface DynamicSystemPromptProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ntype Language = \"English\" | \"Arabic\" | \"German\";\n\nconst LANGUAGES: { value: Language; label: string; flag: string }[] = [\n { value: \"English\", label: \"English\", flag: \"🇬🇧\" },\n { value: \"Arabic\", label: \"Arabic\", flag: \"🇸🇦\" },\n { value: \"German\", label: \"German\", flag: \"🇩🇪\" },\n];\n\nconst DynamicSystemPrompt: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DynamicSystemPromptView = () => {\n const [language, setLanguage] = useState(\"English\");\n\n // The only thing this demo sends: one context item the agent's\n // instructions callable reads on every turn to pick the reply language.\n // No tool, no state — just the AG-UI context channel.\n useAgentContext({\n description: \"Reply language\",\n value: language,\n });\n\n return (\n
\n
\n

Reply language

\n
\n {LANGUAGES.map(({ value, label, flag }) => (\n setLanguage(value)}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-sm text-left transition-colors ${\n language === value\n ? \"border-blue-400 bg-blue-50 dark:bg-blue-950/40\"\n : \"border-black/10 dark:border-white/10\"\n }`}\n >\n {flag}\n {label}\n \n ))}\n
\n

\n Switching language updates the AG-UI context sent with the next\n message — the agent's system prompt is rebuilt every turn to\n match.\n

\n
\n\n
\n
\n \n
\n
\n
\n );\n};\n\nexport default DynamicSystemPrompt;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🌐 Dynamic System Prompt\n\n## What This Demo Shows\n\nThe frontend sends a language choice over the AG-UI **context** channel — a\nplain `{description, value}` pair, no special class involved. The agent's\n`instructions` is a callable (`Agent.instructions`, the OpenAI Agents SDK's\nown dynamic-instructions feature) that reads the context list every turn and\nbakes \"always reply in X\" into the system prompt fresh, before each model\ncall.\n\nContext needs no `AGUIContext`/state machinery — it's read-only and\nregenerated every run, so this demo drives the translator directly instead\nof the shared wrapper: `to_sdk()` / `Runner.run_streamed(..., context=...)`\n/ `to_agui()`, the same three calls as the other servers, plus the one line\nthey skip — passing the AG-UI context straight through as the SDK's own\n`context=`.\n\n## How to Interact\n\n- Pick a language in the sidebar, then ask anything — the reply comes back\n entirely in that language, regardless of what language you type in.\n- Switch languages mid-conversation and ask again — the next reply follows\n the new choice immediately, no restart needed.\n\n## Technical Details\n\n- `useAgentContext({ description: \"Reply language\", value: language })` —\n pushes the current pick onto `RunAgentInput.context` with every message\n- `dynamic_instructions(ctx, agent)` in\n `examples/agents_examples/dynamic_system_prompt.py` reads\n `ctx.context` (the raw `list[Context]` sent by the client) and returns a\n fresh instructions string\n- Runs as its own standalone FastAPI app (own file, own port) rather than\n through the shared demo registry — the shared servers don't forward\n `RunAgentInput.context` into `Runner.run_streamed`, so a demo that needs\n it runs independently instead of changing that shared loop\n", + "language": "markdown", + "type": "file" + }, + { + "name": "dynamic_system_prompt.py", + "content": "\"\"\"Dynamic system prompt — reply language driven by the AG-UI ``context`` channel.\n\nThe point of this demo: **context needs no special AG-UI class.** The frontend\nsends a language choice (English / Arabic / German) over the AG-UI ``context``\nchannel — a plain ``list[Context]`` of ``{description, value}`` items on\n``RunAgentInput.context``. This example folds it into the system prompt with the\nOpenAI Agents SDK's own native feature: ``Agent.instructions`` accepting a\ncallable ``(RunContextWrapper, Agent) -> str``, re-run every turn.\n\nStandalone file, own FastAPI app: neither ``server.py`` nor\n``translator_server.py``'s shared run loops forward ``RunAgentInput.context``\ninto ``Runner.run_streamed`` (``to_sdk()`` hands it back on\n``TranslatedInput.context``, but nothing in those two loops passes it on —\nby design, per ``engine/agui_to_sdk.py``'s own docstring: \"Nothing folds these\ninto the prompt for you\"). Rather than change either shared loop for one demo,\nthis file runs itself: same three calls (``to_sdk`` / ``Runner.run_streamed``\n/ ``to_agui``) as those servers, plus the one line they skip —\n``context=translated.context`` — passed straight through as the SDK's own\n``context=`` and read back via ``RunContextWrapper.context`` in\n``dynamic_instructions``. No ``AGUIContext`` involved anywhere: this demo has\nno state, only context, and context is just a list to read.\n\nRun:\n OPENAI_API_KEY=sk-... uv run python -m agents_examples.dynamic_system_prompt\n\nTest:\n curl -N -X POST http://localhost:8024/dynamic_system_prompt \\\\\n -H 'Content-Type: application/json' \\\\\n -d '{\n \"thread_id\": \"t1\", \"run_id\": \"r1\",\n \"messages\": [{\"id\":\"m1\",\"role\":\"user\",\"content\":\"How is the weather?\"}],\n \"tools\": [], \"state\": {},\n \"context\": [{\"description\": \"Reply language\", \"value\": \"Arabic\"}],\n \"forwarded_props\": null\n }'\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport os\n\nimport uvicorn\nfrom agents import Agent, Runner, RunContextWrapper\nfrom ag_ui.core import Context, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui_openai_agents import AGUITranslator\n\nfrom .constants import DEFAULT_MODEL\n\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\nBASE_INSTRUCTIONS = (\n \"You are a helpful, concise assistant. Answer the user's questions directly.\"\n)\n\n# Fallback when the frontend hasn't picked a language yet.\nDEFAULT_LANGUAGE = \"English\"\n\n\ndef _read_language(ctx: RunContextWrapper[list[Context]]) -> str:\n \"\"\"Pull the reply language out of the AG-UI context list.\n\n ``ctx.context`` here IS the raw ``list[Context]`` the client sent —\n each item a ``{description, value}`` pair, nothing wrapping it. We match\n the item whose description mentions \"language\" and use its value.\n \"\"\"\n items = ctx.context or []\n for item in items:\n if \"language\" in (item.description or \"\").lower():\n return item.value or DEFAULT_LANGUAGE\n return DEFAULT_LANGUAGE\n\n\ndef dynamic_instructions(ctx: RunContextWrapper[list[Context]], agent: Agent) -> str:\n \"\"\"Native SDK dynamic-instructions hook: build the prompt fresh each turn,\n baking in whatever language the frontend currently has selected.\"\"\"\n language = _read_language(ctx)\n return (\n f\"{BASE_INSTRUCTIONS}\\n\"\n f\"Always reply in {language}, no matter what language the user writes in. \"\n f\"Every word of your response must be in {language}.\"\n )\n\n\ndef create_dynamic_system_prompt_agent() -> Agent:\n return Agent(\n name=\"multilingual_assistant\",\n model=DEFAULT_MODEL,\n instructions=dynamic_instructions,\n )\n\n\nagent = create_dynamic_system_prompt_agent()\ntranslator = AGUITranslator()\nencoder = EventEncoder()\n\napp = FastAPI(title=\"AG-UI × OpenAI Agents SDK — dynamic system prompt\")\n\n\n@app.get(\"/health\")\nasync def health() -> dict:\n return {\"status\": \"ok\", \"agent\": agent.name}\n\n\n@app.post(\"/dynamic_system_prompt\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n return StreamingResponse(_stream(body), media_type=encoder.get_content_type())\n\n\nasync def _stream(body: RunAgentInput):\n translated = translator.to_sdk(body)\n\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n\n try:\n result = Runner.run_streamed(\n run_agent,\n input=translated.messages,\n context=translated.context,\n )\n async for ag_event in translator.to_agui(result, body):\n yield encoder.encode(ag_event)\n except Exception:\n logger.exception(\"Agent run failed\")\n\n\ndef main() -> int:\n if not os.getenv(\"OPENAI_API_KEY\"):\n print(\"Error: OPENAI_API_KEY required\")\n return 1\n port = int(os.getenv(\"PORT\", \"8024\"))\n host = os.getenv(\"HOST\", \"0.0.0.0\")\n print(f\"Starting dynamic_system_prompt server on port {port}\")\n uvicorn.run(app, host=host, port=port, log_level=\"info\")\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n", + "language": "python", + "type": "file" + } + ], "langroid::agentic_chat": [ { "name": "page.tsx", diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index f1b68bd587..30faa90ee3 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -387,6 +387,7 @@ export const menuIntegrations = [ "tool_based_generative_ui", "subagents", "custom_lifecycle_events", + "dynamic_system_prompt", ], }, { diff --git a/apps/dojo/src/types/integration.ts b/apps/dojo/src/types/integration.ts index 241d8c1c22..1c5f88a7c1 100644 --- a/apps/dojo/src/types/integration.ts +++ b/apps/dojo/src/types/integration.ts @@ -22,6 +22,7 @@ export type Feature = | "crew_chat" | "subagents" | "custom_lifecycle_events" + | "dynamic_system_prompt" | "error_flow" | "background_agents" | "observational_memory"; diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py index a131303036..7e339bcd2c 100644 --- a/integrations/openai-agents/python/examples/agents_examples/__init__.py +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -11,6 +11,15 @@ No if-branch keyed by demo name, no separate router — one generic loop, one optional per-demo hook. +dynamic_system_prompt is registered here too (so DEMOS/health lists it) but +is never run through the generic loop or wrapper — it needs +``context=run_input.context`` on ``Runner.run_streamed``, which neither the +wrapper (``OpenAIAgentsAgent.run()``) nor the shared translator loops pass. +Its own run loop lives next to the agent in +``dynamic_system_prompt.py`` (``stream()``); ``server.py`` and +``.dev/groq_server.py`` both call that function directly from a hand-written +route instead. + Stateful demos (shared_state, agentic_generative_ui, predictive_state_updates) are shelved together with ``AGUIContext`` — see ``.dev/shelved/``. @@ -31,6 +40,7 @@ build_output_usage_event, create_custom_lifecycle_events_agent, ) +from .dynamic_system_prompt import agent as dynamic_system_prompt_agent from .human_in_the_loop import create_human_in_the_loop_agent from .subagents import create_subagents_agent from .tool_based_generative_ui import create_tool_based_generative_ui_agent @@ -58,6 +68,11 @@ def build_registry() -> dict[str, DemoConfig]: build_start_custom_event=build_input_usage_event, build_end_custom_event=build_output_usage_event, ), + # Same agent singleton dynamic_system_prompt.stream() runs — only + # registered here so DEMOS/health lists it. Servers route this one by + # hand (dynamic_system_prompt.stream) instead of through this agent + # generically, since it needs context= the generic loop can't give it. + "dynamic_system_prompt": DemoConfig(agent=dynamic_system_prompt_agent), } diff --git a/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py b/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py new file mode 100644 index 0000000000..12bf9d3c22 --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py @@ -0,0 +1,104 @@ +"""Dynamic system prompt — reply language driven by the AG-UI ``context`` channel. + +The point of this demo: **context needs no special AG-UI class.** The frontend +sends a language choice (English / Arabic / German) over the AG-UI ``context`` +channel — a plain ``list[Context]`` of ``{description, value}`` items on +``RunAgentInput.context``. This example folds it into the system prompt with the +OpenAI Agents SDK's own native feature: ``Agent.instructions`` accepting a +callable ``(RunContextWrapper, Agent) -> str``, re-run every turn. + +Not registered in ``build_registry()``: ``server.py``'s wrapper +(``OpenAIAgentsAgent``) and ``translator_server.py``'s shared ``_stream()`` +both call ``Runner.run_streamed`` without ``context=``, so neither ever +forwards ``RunAgentInput.context`` to the SDK — ``to_sdk()`` hands it back on +``TranslatedInput.context``, but nothing in those two shared loops passes it +on. Rather than change either shared loop for one demo, ``stream()`` below +is this demo's own run loop — the same three calls those servers make +(``to_sdk`` / ``Runner.run_streamed`` / ``to_agui``), translator-driven, no +``OpenAIAgentsAgent`` wrapper and no FastAPI in this file, plus the one line +those loops skip: ``context=translated.context``, passed straight through as +the SDK's own ``context=`` and read back via ``RunContextWrapper.context`` in +``dynamic_instructions``. ``.dev/groq_server.py`` (gitignored, local Groq +dev) just calls ``stream()`` from its own route. No ``AGUIContext`` anywhere: +this demo has no state, only context, and context is just a list to read. +""" + +from __future__ import annotations + +from typing import AsyncIterator + +from agents import Agent, Runner, RunContextWrapper +from ag_ui.core import BaseEvent, Context, RunAgentInput + +from ag_ui_openai_agents import AGUITranslator + +from .constants import DEFAULT_MODEL + +BASE_INSTRUCTIONS = ( + "You are a helpful, concise assistant. Answer the user's questions directly." +) + +# Fallback when the frontend hasn't picked a language yet. +DEFAULT_LANGUAGE = "English" + + +def _read_language(ctx: RunContextWrapper[list[Context]]) -> str: + """Pull the reply language out of the AG-UI context list. + + ``ctx.context`` here IS the raw ``list[Context]`` the client sent — + each item a ``{description, value}`` pair, nothing wrapping it. We match + the item whose description mentions "language" and use its value. + """ + items = ctx.context or [] + for item in items: + if "language" in (item.description or "").lower(): + return item.value or DEFAULT_LANGUAGE + return DEFAULT_LANGUAGE + + +def dynamic_instructions(ctx: RunContextWrapper[list[Context]], agent: Agent) -> str: + """Native SDK dynamic-instructions hook: build the prompt fresh each turn, + baking in whatever language the frontend currently has selected.""" + language = _read_language(ctx) + return ( + f"{BASE_INSTRUCTIONS}\n" + f"Always reply in {language}, no matter what language the user writes in. " + f"Every word of your response must be in {language}." + ) + + +def create_dynamic_system_prompt_agent() -> Agent: + return Agent( + name="multilingual_assistant", + model=DEFAULT_MODEL, + instructions=dynamic_instructions, + ) + + +agent = create_dynamic_system_prompt_agent() + +# Reusable — to_sdk is stateless, to_agui spins up a fresh engine per call. +_translator = AGUITranslator() + + +async def stream(body: RunAgentInput) -> AsyncIterator[BaseEvent]: + """Run this demo for one AG-UI request, translator by hand, and yield AG-UI events. + + Any server can call this directly — it owns the HTTP/SSE plumbing, this + owns the run loop. The one line that makes the demo work: + ``context=translated.context``, forwarded to ``Runner.run_streamed`` so + ``dynamic_instructions`` can read it back. + """ + translated = _translator.to_sdk(body) + + run_agent = agent + if translated.tools: + run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools]) + + result = Runner.run_streamed( + run_agent, + input=translated.messages, + context=translated.context, + ) + async for event in _translator.to_agui(result, body): + yield event diff --git a/integrations/openai-agents/python/examples/server.py b/integrations/openai-agents/python/examples/server.py index b4904e84a4..9eb208afbd 100644 --- a/integrations/openai-agents/python/examples/server.py +++ b/integrations/openai-agents/python/examples/server.py @@ -15,7 +15,11 @@ POST /backend_tool_rendering ← server-executed @function_tool POST /human_in_the_loop ← frontend-owned tool, StopAtTools POST /tool_based_generative_ui ← frontend tool renders the content - POST /orchestrator ← multi-agent via agents-as-tools + POST /subagents ← multi-agent via agents-as-tools + POST /custom_lifecycle_events ← manual CUSTOM event bracketing the run + (routed by hand — see below) + POST /dynamic_system_prompt ← system prompt built from RunAgentInput.context + (routed by hand — see below) GET /health ← liveness check (lists all demos) GET //health ← per-demo check @@ -53,11 +57,20 @@ from pathlib import Path import uvicorn +from agents import Runner +from ag_ui.core import RunAgentInput +from ag_ui.encoder import EventEncoder from fastapi import FastAPI +from fastapi.responses import StreamingResponse -from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint +from ag_ui_openai_agents import ( + AGUITranslator, + OpenAIAgentsAgent, + add_openai_agents_fastapi_endpoint, +) from agents_examples import DemoConfig, build_registry +from agents_examples.dynamic_system_prompt import stream as dsp_stream logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -66,14 +79,62 @@ app = FastAPI(title="AG-UI × OpenAI Agents SDK examples (wrapper)") -# The whole run loop: one wrapped agent + one endpoint per demo. +# The whole run loop: one wrapped agent + one endpoint per demo. Two demos +# need more than the wrapper can give them, so they're routed by hand below +# instead of through this loop: +# - custom_lifecycle_events: OpenAIAgentsAgent.run() calls +# to_agui(result, input) with no extra kwargs, so the wrapper can't +# forward DemoConfig.build_start_custom_event/build_end_custom_event — +# the CUSTOM events would silently never go out. +# - dynamic_system_prompt: OpenAIAgentsAgent.run() never passes context= +# to Runner.run_streamed, so the demo's whole point (reading +# RunAgentInput.context) would silently do nothing. +_HAND_ROUTED = {"custom_lifecycle_events", "dynamic_system_prompt"} + for demo_name, demo in DEMOS.items(): + if demo_name in _HAND_ROUTED: + continue add_openai_agents_fastapi_endpoint( app, OpenAIAgentsAgent(demo.agent, name=demo_name), f"/{demo_name}", ) +_lifecycle_translator = AGUITranslator() +_lifecycle_encoder = EventEncoder() +_lifecycle_demo = DEMOS["custom_lifecycle_events"] + + +@app.post("/custom_lifecycle_events") +async def _run_custom_lifecycle_events(body: RunAgentInput) -> StreamingResponse: + async def _stream(): + translated = _lifecycle_translator.to_sdk(body) + agent = _lifecycle_demo.agent + if translated.tools: + agent = agent.clone(tools=[*agent.tools, *translated.tools]) + kwargs = {} + if _lifecycle_demo.build_start_custom_event is not None: + kwargs["start_custom_event"] = _lifecycle_demo.build_start_custom_event() + if _lifecycle_demo.build_end_custom_event is not None: + kwargs["end_custom_event"] = _lifecycle_demo.build_end_custom_event() + result = Runner.run_streamed(agent, input=translated.messages) + async for ag_event in _lifecycle_translator.to_agui(result, body, **kwargs): + yield _lifecycle_encoder.encode(ag_event) + + return StreamingResponse(_stream(), media_type=_lifecycle_encoder.get_content_type()) + + +_dsp_encoder = EventEncoder() + + +@app.post("/dynamic_system_prompt") +async def _run_dynamic_system_prompt(body: RunAgentInput) -> StreamingResponse: + async def _stream(): + async for ag_event in dsp_stream(body): + yield _dsp_encoder.encode(ag_event) + + return StreamingResponse(_stream(), media_type=_dsp_encoder.get_content_type()) + @app.get("/health") async def health() -> dict: From 586697bd9e60fe950811fab793ae82828a96706d Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 00:19:16 +0200 Subject: [PATCH 31/94] test(openai-agents): add multimodal content translation tests --- integrations/openai-agents/python/README.md | 33 ++- .../python/tests/test_multimodal.py | 262 ++++++++++++++++++ 2 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 integrations/openai-agents/python/tests/test_multimodal.py diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 2c598b46d9..d531c1e6a9 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -394,7 +394,7 @@ Both directions, source of truth is `engine/agui_to_sdk.py` and | `ActivityMessage` | dropped (no Responses-API equivalent; override to fold into the prompt) | | `Tool` (client-declared) | SDK `FunctionTool` proxy that raises `ClientToolPending` when invoked | | Content parts: `TextInputContent`, `ImageInputContent`, `AudioInputContent`, `DocumentInputContent`, `BinaryInputContent` | `input_text` / `input_image` / `input_audio` / `input_file` parts | -| `VideoInputContent` | dropped (Responses API has no video input) | +| `VideoInputContent` | dropped (no OpenAI API accepts video input yet) | **Outbound** — SDK stream event → AG-UI `BaseEvent` (`SDKToAGUITranslator`): @@ -444,6 +444,37 @@ async for event in translator.to_agui( yield encoder.encode(event) ``` +### Multi-modality + +**Input (what the user sends the agent):** an AG-UI `UserMessage` can carry +more than text — images, audio clips, documents, etc. Each one is a typed +content part (`ag_ui.core.InputContent`). The translator converts each part +into the block shape the OpenAI Agents SDK expects for that message: + +| User sends this... | ...becomes this for the SDK | How | +|---|---|---| +| Plain text | `input_text` | as-is | +| An image | `input_image` | a URL is passed through; raw bytes become a base64 `data:` URL | +| An audio clip | `input_audio` | must be raw bytes (base64) — audio can't be sent as a URL; the audio format (`wav`, `mp3`, ...) is read from the clip's mime type | +| A document (PDF, etc.) | `input_file` | URL stays a URL; raw bytes become base64 | +| A video | *(dropped, see below)* | not supported — see below | +| `BinaryInputContent` (deprecated, legacy catch-all) | whichever of the above matches its mime type | e.g. `image/*` → `input_image` | + +**Video input isn't supported, and it's not our limitation to fix.** Right +now, no OpenAI API (Responses or Chat Completions) accepts video as input at +all — there's no video content type to translate into. If OpenAI adds one, +we'll wire it up. Until then, video parts are silently dropped (logged, not +an error). If you want to work around it today, override +`translate_video_content` in a subclass — e.g. extract a few frames and send +them as images instead. + +**Output (what the agent sends back):** text only. If the agent generates an +image, audio, or speaks out loud (TTS), none of that reaches the AG-UI +client — the AG-UI protocol itself has no event type for "here's an image/audio +clip the model produced," so there's nothing to translate it into. This is a +protocol-level gap, not something specific to this integration — every +AG-UI SDK has the same hole. + ### Transport Options For SSE, use the AG-UI SDK's own encoder — it produces one `data:` frame per diff --git a/integrations/openai-agents/python/tests/test_multimodal.py b/integrations/openai-agents/python/tests/test_multimodal.py new file mode 100644 index 0000000000..6acd19232a --- /dev/null +++ b/integrations/openai-agents/python/tests/test_multimodal.py @@ -0,0 +1,262 @@ +""" +Tests for AGUIToSDKTranslator's multimodal content mapping. + +Pins the wire shape each ``translate_*_content`` method produces (or the +``None``/drop behavior when unsupported), per the table in +``.dev/MESSAGES.md`` / ``.dev/INTEGRATIONS_MATRIX.md``. No network, no model; +just the mapping. +""" + +from __future__ import annotations + +import warnings + +from ag_ui.core import ( + AudioInputContent, + BinaryInputContent, + DocumentInputContent, + ImageInputContent, + InputContentDataSource, + InputContentUrlSource, + TextInputContent, + VideoInputContent, +) + +from ag_ui_openai_agents.engine.agui_to_sdk import AGUIToSDKTranslator + +_engine = AGUIToSDKTranslator() + + +def _binary(**kwargs) -> BinaryInputContent: + """BinaryInputContent is deprecated but still a real inbound shape callers + may send; construct it without polluting test output with the warning.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return BinaryInputContent(**kwargs) + + +# ── image ────────────────────────────────────────────────────────────── + + +def test_image_url_source_passes_through(): + part = ImageInputContent(source=InputContentUrlSource(value="https://x/y.png")) + out = _engine.translate_image_content(part) + assert out == {"type": "input_image", "image_url": "https://x/y.png"} + + +def test_image_data_source_becomes_data_url(): + part = ImageInputContent( + source=InputContentDataSource(value="Zm9v", mime_type="image/png") + ) + out = _engine.translate_image_content(part) + assert out == {"type": "input_image", "image_url": "data:image/png;base64,Zm9v"} + + +def test_image_with_no_usable_source_returns_none(): + part = ImageInputContent(source=InputContentDataSource(value="", mime_type="image/png")) + assert _engine.translate_image_content(part) is None + + +# ── audio ────────────────────────────────────────────────────────────── + + +def test_audio_data_source_maps_to_input_audio(): + part = AudioInputContent( + source=InputContentDataSource(value="Zm9v", mime_type="audio/wav") + ) + out = _engine.translate_audio_content(part) + assert out == { + "type": "input_audio", + "input_audio": {"data": "Zm9v", "format": "wav"}, + } + + +def test_audio_url_source_is_dropped(): + # Responses API accepts no URL form for audio input. + part = AudioInputContent(source=InputContentUrlSource(value="https://x/y.wav")) + assert _engine.translate_audio_content(part) is None + + +def test_audio_missing_mime_defaults_to_wav(): + part = AudioInputContent(source=InputContentDataSource(value="Zm9v", mime_type="")) + out = _engine.translate_audio_content(part) + assert out["input_audio"]["format"] == "wav" + + +def test_audio_format_from_mime_variants(): + fmt = _engine._audio_format_from_mime + assert fmt("audio/mpeg") == "mp3" + assert fmt("audio/mp3") == "mp3" + assert fmt("audio/mpeg3") == "mp3" + assert fmt("audio/x-wav") == "wav" + assert fmt("audio/wav") == "wav" + assert fmt("audio/wave") == "wav" + assert fmt("audio/ogg") == "ogg" + assert fmt(None) == "wav" + + +# ── document ─────────────────────────────────────────────────────────── + + +def test_document_url_source_uses_file_url(): + part = DocumentInputContent(source=InputContentUrlSource(value="https://x/y.pdf")) + out = _engine.translate_document_content(part) + assert out == {"type": "input_file", "file_url": "https://x/y.pdf"} + + +def test_document_data_source_uses_file_data(): + part = DocumentInputContent( + source=InputContentDataSource(value="Zm9v", mime_type="application/pdf") + ) + out = _engine.translate_document_content(part) + assert out == { + "type": "input_file", + "file_data": "data:application/pdf;base64,Zm9v", + } + + +def test_document_with_no_usable_value_returns_none(): + part = DocumentInputContent( + source=InputContentDataSource(value="", mime_type="application/pdf") + ) + assert _engine.translate_document_content(part) is None + + +# ── video (unsupported by the Responses API) ───────────────────────────── + + +def test_video_is_always_dropped(): + part = VideoInputContent(source=InputContentUrlSource(value="https://x/y.mp4")) + assert _engine.translate_video_content(part) is None + + +# ── binary (mime-sniffed, routed to image/audio/file) ──────────────────── + + +def test_binary_image_mime_routes_to_image_url(): + part = _binary(mime_type="image/png", url="https://x/y.png") + out = _engine.translate_binary_content(part) + assert out == {"type": "input_image", "image_url": "https://x/y.png"} + + +def test_binary_audio_mime_routes_to_input_audio(): + part = _binary(mime_type="audio/wav", data="Zm9v") + out = _engine.translate_binary_content(part) + assert out == { + "type": "input_audio", + "input_audio": {"data": "Zm9v", "format": "wav"}, + } + + +def test_binary_other_mime_routes_to_file(): + part = _binary(mime_type="application/pdf", data="Zm9v", filename="a.pdf") + out = _engine.translate_binary_content(part) + assert out == { + "type": "input_file", + "file_data": "data:application/pdf;base64,Zm9v", + "filename": "a.pdf", + } + + +def test_binary_as_image_prefers_url_over_data(): + part = _binary(mime_type="image/png", url="https://x/y.png", data="Zm9v") + out = _engine._binary_as_image(part) + assert out == {"type": "input_image", "image_url": "https://x/y.png"} + + +def test_binary_as_image_falls_back_to_data(): + part = _binary(mime_type="image/png", data="Zm9v") + out = _engine._binary_as_image(part) + assert out == {"type": "input_image", "image_url": "data:image/png;base64,Zm9v"} + + +def test_binary_as_image_with_neither_returns_none(): + part = _binary(mime_type="image/png", id="file-123") + assert _engine._binary_as_image(part) is None + + +def test_binary_as_audio_without_data_is_dropped(): + # URL-only audio isn't accepted by the Responses API. + part = _binary(mime_type="audio/wav", url="https://x/y.wav") + assert _engine._binary_as_audio(part, "audio/wav") is None + + +def test_binary_as_file_url_source(): + part = _binary(mime_type="application/pdf", url="https://x/y.pdf") + out = _engine._binary_as_file(part, "application/pdf") + assert out == {"type": "input_file", "file_url": "https://x/y.pdf"} + + +def test_binary_as_file_data_without_filename_omits_key(): + part = _binary(mime_type="application/pdf", data="Zm9v") + out = _engine._binary_as_file(part, "application/pdf") + assert out == {"type": "input_file", "file_data": "data:application/pdf;base64,Zm9v"} + assert "filename" not in out + + +# ── source resolution helper ────────────────────────────────────────────── + + +def test_data_source_to_url_handles_url_data_and_none(): + fn = _engine._data_source_to_url + assert fn(InputContentUrlSource(value="https://x/y.png")) == "https://x/y.png" + assert ( + fn(InputContentDataSource(value="Zm9v", mime_type="image/png")) + == "data:image/png;base64,Zm9v" + ) + assert fn(None) is None + + +# ── dict-shaped fallback dispatch ───────────────────────────────────────── + + +def test_dispatch_dict_content_part_text(): + out = _engine._dispatch_dict_content_part({"type": "text", "text": "hi"}) + assert out == {"type": "input_text", "text": "hi"} + + +def test_dispatch_dict_content_part_image(): + out = _engine._dispatch_dict_content_part( + {"type": "image", "source": {"type": "url", "value": "https://x/y.png"}} + ) + assert out == {"type": "input_image", "image_url": "https://x/y.png"} + + +def test_dispatch_dict_content_part_unknown_type_returns_none(): + assert _engine._dispatch_dict_content_part({"type": "carrier_pigeon"}) is None + + +# ── translate_content_part tier-2 dispatcher ────────────────────────────── + + +def test_translate_content_part_dispatches_by_type(): + text = TextInputContent(text="hi") + image = ImageInputContent(source=InputContentUrlSource(value="https://x/y.png")) + assert _engine.translate_content_part(text) == {"type": "input_text", "text": "hi"} + assert _engine.translate_content_part(image) == { + "type": "input_image", + "image_url": "https://x/y.png", + } + + +# ── end-to-end: mixed multimodal user message ───────────────────────────── + + +def test_mixed_multimodal_parts_translate_to_matching_blocks(): + parts = [ + TextInputContent(text="what's this?"), + ImageInputContent(source=InputContentUrlSource(value="https://x/y.png")), + AudioInputContent( + source=InputContentDataSource(value="Zm9v", mime_type="audio/mpeg") + ), + DocumentInputContent(source=InputContentUrlSource(value="https://x/y.pdf")), + VideoInputContent(source=InputContentUrlSource(value="https://x/y.mp4")), + ] + blocks = [_engine.translate_content_part(p) for p in parts] + assert blocks == [ + {"type": "input_text", "text": "what's this?"}, + {"type": "input_image", "image_url": "https://x/y.png"}, + {"type": "input_audio", "input_audio": {"data": "Zm9v", "format": "mp3"}}, + {"type": "input_file", "file_url": "https://x/y.pdf"}, + None, # video: no Responses-API input block + ] From 20c61906de8ae33fcae570ffaafe272e6dcf70d3 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 01:25:43 +0200 Subject: [PATCH 32/94] feat(examples): add human in the loop back tool approval example --- apps/dojo/src/agents.ts | 1 + .../(v2)/human_in_the_loop_approval/page.tsx | 156 ++++++++++++++++++ apps/dojo/src/config.ts | 7 + apps/dojo/src/menu.ts | 1 + apps/dojo/src/types/integration.ts | 1 + integrations/openai-agents/python/README.md | 73 ++++++++ .../openai-agents/python/examples/README.md | 44 +++++ .../examples/agents_examples/__init__.py | 8 + .../human_in_the_loop_approval.py | 71 ++++++++ .../openai-agents/python/examples/server.py | 92 ++++++++++- .../python/examples/translator_server.py | 95 ++++++++++- 11 files changed, 545 insertions(+), 4 deletions(-) create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx create mode 100644 integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 8217c7cfaa..1597f141f5 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -640,6 +640,7 @@ export const agentsIntegrations = { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", + human_in_the_loop_approval: "human_in_the_loop_approval", tool_based_generative_ui: "tool_based_generative_ui", subagents: "subagents", custom_lifecycle_events: "custom_lifecycle_events", diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx new file mode 100644 index 0000000000..b19358250b --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx @@ -0,0 +1,156 @@ +"use client"; +import React, { useState } from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { + CopilotChat, + CopilotKitProvider, + useAgent, + useCopilotKit, + useConfigureSuggestions, +} from "@copilotkit/react-core/v2"; + +interface ApprovalProps { + params: Promise<{ integrationId: string }>; +} + +interface PendingApproval { + callId: string; + toolName: string; + arguments: string; +} + +const Approval: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + ); +}; + +const Chat = () => { + const { agent } = useAgent({ agentId: "human_in_the_loop_approval" }); + const { copilotkit } = useCopilotKit(); + const [pending, setPending] = useState(null); + const [resolvedCallId, setResolvedCallId] = useState(null); + + useConfigureSuggestions({ + suggestions: [ + { title: "Refund an order", message: "I'd like a refund for ORD-1001." }, + { title: "Refund another order", message: "Please refund ORD-1002." }, + { title: "Unknown order", message: "Can you refund ORD-9999?" }, + ], + available: "always", + consumerAgentId: "human_in_the_loop_approval", + }); + + React.useEffect(() => { + const subscription = agent.subscribe({ + onCustomEvent: ({ event }) => { + if (event.name !== "approval_request") return; + // One CustomEvent can carry several pending calls if the model made + // more than one needs_approval call in a turn; this demo only ever + // triggers one, so showing the first is enough here. + const pendingList = event.value as Array<{ + call_id: string; + tool_name: string; + arguments: string; + }>; + const first = pendingList[0]; + if (!first) return; + setResolvedCallId(null); + setPending({ + callId: first.call_id, + toolName: first.tool_name, + arguments: first.arguments, + }); + }, + }); + return () => subscription.unsubscribe(); + }, [agent]); + + const respond = (approve: boolean) => { + if (!pending) return; + setResolvedCallId(pending.callId); + copilotkit.runAgent({ + agent, + forwardedProps: { + approval: { call_id: pending.callId, approve }, + }, + }); + setPending(null); + }; + + return ( +
+ {pending && ( + respond(true)} onReject={() => respond(false)} /> + )} + {!pending && resolvedCallId && ( +
Decision sent — waiting for the agent...
+ )} +
+ +
+
+ ); +}; + +function ApprovalCard({ + pending, + onApprove, + onReject, +}: { + pending: PendingApproval; + onApprove: () => void; + onReject: () => void; +}) { + let args: Record = {}; + try { + args = JSON.parse(pending.arguments); + } catch { + // leave args empty — raw string shown below is enough context either way + } + + return ( +
+

Approval needed

+

+ The agent wants to call {pending.toolName} with: +

+
+        {JSON.stringify(args, null, 2)}
+      
+
+ + +
+
+ ); +} + +export default Approval; diff --git a/apps/dojo/src/config.ts b/apps/dojo/src/config.ts index ede7442b97..dafc9616e6 100644 --- a/apps/dojo/src/config.ts +++ b/apps/dojo/src/config.ts @@ -36,6 +36,13 @@ export const featureConfig: FeatureConfig[] = [ "Plan a task together and direct the Copilot to take the right steps", tags: ["HITL", "Interactivity"], }), + createFeatureConfig({ + id: "human_in_the_loop_approval", + name: "Human in the Loop (Backend Approval)", + description: + "A real backend tool (issue a refund) pauses for human approval before it runs, then resumes", + tags: ["HITL", "Interactivity", "Approval"], + }), createFeatureConfig({ id: "interrupt", name: "Interrupt (Suspend/Resume)", diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index 30faa90ee3..592fce9ffb 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -384,6 +384,7 @@ export const menuIntegrations = [ "agentic_chat", "backend_tool_rendering", "human_in_the_loop", + "human_in_the_loop_approval", "tool_based_generative_ui", "subagents", "custom_lifecycle_events", diff --git a/apps/dojo/src/types/integration.ts b/apps/dojo/src/types/integration.ts index 1c5f88a7c1..91296b5944 100644 --- a/apps/dojo/src/types/integration.ts +++ b/apps/dojo/src/types/integration.ts @@ -4,6 +4,7 @@ export type Feature = | "agentic_chat" | "agentic_generative_ui" | "human_in_the_loop" + | "human_in_the_loop_approval" | "interrupt" | "predictive_state_updates" | "shared_state" diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index d531c1e6a9..7a152e7f91 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -626,6 +626,79 @@ Request 2: messages include the ToolMessage result → agent continues See `examples/agents_examples/human_in_the_loop.py` for the agent and `examples/server.py` for the run loop that merges the client tools. +## Backend tool approval (`needs_approval`) + +Different from the frontend-tools pattern above: here the tool is real +server-side code (`@function_tool`, `Agent.tools=[...]`), and the SDK itself +gates it with `needs_approval=True` — no AG-UI concept involved. This is for +"the backend can do this, but a human should sign off first" (e.g. issuing a +refund), as opposed to "only the browser can do this at all". + +| | Frontend tools (above) | Backend approval (here) | +|---|---|---| +| Tool implementation | None — frontend-only | Real, server-side | +| Pause mechanism | `StopAtTools`, mid-stream | `needs_approval`, only known post-stream via `result.interruptions` | +| Decision carried back as | An ordinary `ToolMessage` next turn | `forwarded_props["approval"]` + a stored `RunState` | +| Dojo demo | `human_in_the_loop` | `human_in_the_loop_approval` | + +**1. Mark the tool.** The SDK stops the run before the body executes and +reports the pending call on `result.interruptions`: + +```python +from agents import function_tool + +@function_tool(needs_approval=True) +def issue_refund(order_id: str) -> str: + ... # real logic — only runs after approval +``` + +**2. `result.interruptions` is only known post-hoc — and the client drops +anything sent after `RUN_FINISHED`.** So the approval event can't be a +plain event yielded after the `to_agui()` loop; it has to be +`end_custom_event` (fires right before `RUN_FINISHED`), which means +draining the raw SDK stream yourself first instead of handing `result` +straight to `to_agui`: + +```python +raw_events = [event async for event in result.stream_events()] + +end_custom_event = None +if result.interruptions: + state = result.to_state() # serialize the paused run + store[run_input.thread_id] = state # keep it somewhere until the decision arrives + end_custom_event = CustomEvent( + name="approval_request", + value=[ + {"call_id": item.raw_item.call_id, "tool_name": item.tool_name} + for item in result.interruptions + ], + ) + +async def _replay(): + for event in raw_events: + yield event + +async for ag_event in translator.to_agui(_replay(), run_input, end_custom_event=end_custom_event): + yield encoder.encode(ag_event) +``` + +**3. Resume from the stored state on the next request**, once the client +sends the decision back (however you choose to carry it — e.g. +`forwarded_props`): + +```python +state = store.pop(run_input.thread_id) +item = next(i for i in state.get_interruptions() if i.raw_item.call_id == call_id) +state.approve(item) if approve else state.reject(item) +result = Runner.run_streamed(agent, state) # resumes, not a fresh run +``` + +There's no AG-UI-native event for the approval request — `CustomEvent` is +the same escape hatch used for MCP approval requests elsewhere in this +package. See `examples/agents_examples/human_in_the_loop_approval.py` for +the agent and `examples/server.py` (`/human_in_the_loop_approval`) for the +full hand-routed loop, including the in-memory pending-state store. + ## Gotchas - **Reasoning replay** (sending reasoning back to OpenAI) only works via diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 7823b974d7..9603c1f5ae 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -81,6 +81,13 @@ the steps, gets user approval, and sends the result back as an ordinary a `generate_task_steps` tool definition in `RunAgentInput.tools` (e.g. the AG-UI Dojo's human-in-the-loop page, which uses the same tool name). +> **vs. `human_in_the_loop_approval` (below):** here the tool has *no server +> implementation* — only the browser can run it, so there's nothing to gate. +> `human_in_the_loop_approval` is the opposite case: a real backend tool, +> paused by the SDK's own approval API before *its* body runs. Different +> problem, different mechanism — see the comparison table under +> `human_in_the_loop_approval`. + ### `tool_based_generative_ui` Another frontend-owned tool (`generate_haiku`), but here the tool call *is* the deliverable: the frontend renders the haiku card straight from the @@ -90,6 +97,43 @@ mechanics as `human_in_the_loop`. **Try:** `"Write me a haiku about the ocean."` — needs a client that sends a `generate_haiku` tool definition (Dojo's tool-based generative UI page). +### `human_in_the_loop_approval` +A *backend*-owned tool (`issue_refund`) that requires approval before it +runs — the SDK's `needs_approval=True` (`agents.tool.function_tool`), not an +AG-UI concept. Unlike `human_in_the_loop`, the tool has a real server-side +implementation; the SDK itself pauses the run and reports a +`ToolApprovalItem` on `result.interruptions` before the body ever executes. +That's only known once the stream is fully drained, so this demo is +hand-routed (see `server.py` / `translator_server.py`) instead of running +through the shared loop: + +1. First request runs normally; if `result.interruptions` is non-empty after + the stream ends, the server serializes the paused run + (`result.to_state()`), keeps it in memory keyed by `thread_id`, and sends + a `CustomEvent(name="approval_request")` as `to_agui()`'s + `end_custom_event` — right before `RUN_FINISHED`, not after (the client + drops anything that arrives once a run is marked finished). +2. The client's decision comes back on the *next* request as + `RunAgentInput.forwarded_props["approval"]` (`{"call_id", "approve"}`). + The server looks up the stored state, calls `state.approve()` / + `state.reject()`, and resumes with `Runner.run_streamed(agent, state)` + instead of starting over from `translated.messages`. + +**Try:** `"I'd like a refund for ORD-1001."` — requires a client that renders +`approval_request` custom events and sends the decision back via +`forwarded_props` (the AG-UI Dojo's Human in the Loop (Backend Approval) +page does this). + +**`human_in_the_loop_approval` vs. `human_in_the_loop` — same goal, different mechanism:** + +| | `human_in_the_loop` | `human_in_the_loop_approval` | +|---|---|---| +| Who owns the tool | Frontend — no server implementation exists | Backend — real `@function_tool` code | +| What pauses the run | `StopAtTools`, the instant the model calls it | The SDK's own `needs_approval` gate, before the body runs | +| How the pause surfaces | Normal `TOOL_CALL_*` events, mid-stream | `result.interruptions`, only known after the stream ends | +| How the decision comes back | An ordinary `ToolMessage` in the next request's `messages` | `RunAgentInput.forwarded_props["approval"]`, resumed via a stored `RunState` | +| Why | The action can *only* happen client-side (render UI, wait on a click) | The backend *can* do it, but shouldn't without sign-off first | + ### `orchestrator` Multi-agent via the SDK's **agents-as-tools** pattern (`Agent.as_tool()`) — control never transfers, the orchestrator calls `research_agent`, diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py index 7e339bcd2c..c8ed885797 100644 --- a/integrations/openai-agents/python/examples/agents_examples/__init__.py +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -42,6 +42,7 @@ ) from .dynamic_system_prompt import agent as dynamic_system_prompt_agent from .human_in_the_loop import create_human_in_the_loop_agent +from .human_in_the_loop_approval import create_human_in_the_loop_approval_agent from .subagents import create_subagents_agent from .tool_based_generative_ui import create_tool_based_generative_ui_agent @@ -61,6 +62,12 @@ def build_registry() -> dict[str, DemoConfig]: "agentic_chat": DemoConfig(agent=create_agentic_chat_agent()), "backend_tool_rendering": DemoConfig(agent=create_backend_tool_agent()), "human_in_the_loop": DemoConfig(agent=create_human_in_the_loop_agent()), + # Same idea as human_in_the_loop (pause for a person before an action + # happens) but a different mechanism — see + # human_in_the_loop_approval.py's docstring for the frontend-tool vs + # SDK-native-approval distinction. Hand-routed, same reason as + # dynamic_system_prompt below. + "human_in_the_loop_approval": DemoConfig(agent=create_human_in_the_loop_approval_agent()), "tool_based_generative_ui": DemoConfig(agent=create_tool_based_generative_ui_agent()), "subagents": DemoConfig(agent=create_subagents_agent()), "custom_lifecycle_events": DemoConfig( @@ -83,6 +90,7 @@ def build_registry() -> dict[str, DemoConfig]: "create_backend_tool_agent", "create_custom_lifecycle_events_agent", "create_human_in_the_loop_agent", + "create_human_in_the_loop_approval_agent", "create_subagents_agent", "create_tool_based_generative_ui_agent", ] diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py new file mode 100644 index 0000000000..e38ccb23dd --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py @@ -0,0 +1,71 @@ +"""Tool approval — a *backend*-owned tool gated by the SDK's own approval API. + +Unlike :mod:`human_in_the_loop` (a frontend-only tool with no server +implementation) and :mod:`backend_tool_rendering` (a server tool that always +runs), ``issue_refund`` here is real server-side logic that only runs after a +human approves it — the SDK's ``needs_approval=True`` mechanism +(``agents.tool.function_tool``), not an AG-UI concept. + +Mechanically: when the model calls ``issue_refund``, the SDK stops the run +*before* the tool body executes and surfaces a ``ToolApprovalItem`` on +``result.interruptions``. That only becomes known once the stream is fully +drained — there is no mid-stream event for it — so it can't go through the +normal per-item translator dispatch the way ``MCPApprovalRequestItem`` does. +The run loop (``server.py`` / ``translator_server.py``) checks +``result.interruptions`` right after ``to_agui()`` finishes, and if any are +pending: + +1. Serializes the paused run via ``result.to_state()`` and keeps it + server-side, keyed by ``thread_id`` (an in-memory dict here — a real app + would use a session store; this survives one process, not a restart). +2. Emits one ``CustomEvent(name="approval_request")`` carrying every + interruption, as ``to_agui()``'s ``end_custom_event`` — right before + ``RUN_FINISHED``, not after it. The client drops anything that arrives + once a run is marked finished, so this has to land before that event, + which means draining the raw SDK stream by hand first (interruptions + aren't known until it's fully drained) instead of handing ``result`` + straight to ``to_agui()``. + +The frontend renders Approve/Reject; either choice comes back as the next +``RunAgentInput.forwarded_props["approval"]`` (``{"call_id", "approve"}``). +The run loop looks up the stored state, calls ``state.approve()`` / +``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)`` +instead of starting fresh from ``translated.messages``. +""" + +from __future__ import annotations + +from agents import Agent, function_tool + +from .constants import DEFAULT_MODEL + +# Fake order book — good enough to make "approved" visibly do something. +_ORDERS: dict[str, dict] = { + "ORD-1001": {"amount": 49.99, "status": "paid"}, + "ORD-1002": {"amount": 129.50, "status": "paid"}, +} + + +@function_tool(needs_approval=True) +def issue_refund(order_id: str) -> str: + """Issue a full refund for an order. Requires human approval before running.""" + order = _ORDERS.get(order_id) + if order is None: + return f"No such order: {order_id}" + order["status"] = "refunded" + return f"Refunded ${order['amount']:.2f} for {order_id}." + + +def create_human_in_the_loop_approval_agent() -> Agent: + return Agent( + name="refund_assistant", + model=DEFAULT_MODEL, + instructions=( + "You are a customer support assistant. When the user asks for a " + "refund on an order, call issue_refund with that order id. " + "Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask " + "for confirmation yourself — the approval happens outside the " + "conversation before the tool runs." + ), + tools=[issue_refund], + ) diff --git a/integrations/openai-agents/python/examples/server.py b/integrations/openai-agents/python/examples/server.py index 9eb208afbd..83b9c54655 100644 --- a/integrations/openai-agents/python/examples/server.py +++ b/integrations/openai-agents/python/examples/server.py @@ -20,6 +20,9 @@ (routed by hand — see below) POST /dynamic_system_prompt ← system prompt built from RunAgentInput.context (routed by hand — see below) + POST /human_in_the_loop_approval ← backend tool gated by needs_approval, + resumed from result.interruptions + (routed by hand — see below) GET /health ← liveness check (lists all demos) GET //health ← per-demo check @@ -58,7 +61,7 @@ import uvicorn from agents import Runner -from ag_ui.core import RunAgentInput +from ag_ui.core import CustomEvent, EventType, RunAgentInput from ag_ui.encoder import EventEncoder from fastapi import FastAPI from fastapi.responses import StreamingResponse @@ -79,7 +82,7 @@ app = FastAPI(title="AG-UI × OpenAI Agents SDK examples (wrapper)") -# The whole run loop: one wrapped agent + one endpoint per demo. Two demos +# The whole run loop: one wrapped agent + one endpoint per demo. Three demos # need more than the wrapper can give them, so they're routed by hand below # instead of through this loop: # - custom_lifecycle_events: OpenAIAgentsAgent.run() calls @@ -89,7 +92,10 @@ # - dynamic_system_prompt: OpenAIAgentsAgent.run() never passes context= # to Runner.run_streamed, so the demo's whole point (reading # RunAgentInput.context) would silently do nothing. -_HAND_ROUTED = {"custom_lifecycle_events", "dynamic_system_prompt"} +# - human_in_the_loop_approval: needs to inspect result.interruptions after the stream +# and, on the next request, resume from a stored RunState instead of +# translated.messages — the wrapper always starts fresh from the input. +_HAND_ROUTED = {"custom_lifecycle_events", "dynamic_system_prompt", "human_in_the_loop_approval"} for demo_name, demo in DEMOS.items(): if demo_name in _HAND_ROUTED: @@ -136,6 +142,86 @@ async def _stream(): return StreamingResponse(_stream(), media_type=_dsp_encoder.get_content_type()) +_approval_translator = AGUITranslator() +_approval_encoder = EventEncoder() +_approval_demo = DEMOS["human_in_the_loop_approval"] + +# thread_id -> paused RunState, waiting for an approve/reject decision on the +# next request. In-memory only — fine for a demo process, not a restart or a +# second server instance; a real app would use a session store instead. +_PENDING_APPROVALS: dict[str, object] = {} + + +@app.post("/human_in_the_loop_approval") +async def _run_approval(body: RunAgentInput) -> StreamingResponse: + async def _stream(): + agent = _approval_demo.agent + decision = None + forwarded = body.forwarded_props + if isinstance(forwarded, dict): + decision = forwarded.get("approval") + + pending_state = _PENDING_APPROVALS.pop(body.thread_id, None) + if decision and pending_state is not None: + item = next( + ( + i + for i in pending_state.get_interruptions() + if getattr(i.raw_item, "call_id", None) == decision.get("call_id") + ), + None, + ) + if item is not None: + if decision.get("approve"): + pending_state.approve(item) + else: + pending_state.reject(item) + result = Runner.run_streamed(agent, pending_state) + else: + translated = _approval_translator.to_sdk(body) + if translated.tools: + agent = agent.clone(tools=[*agent.tools, *translated.tools]) + result = Runner.run_streamed(agent, input=translated.messages) + + # result.interruptions is only known once the SDK's own stream is + # fully drained — there's no mid-stream event for it. to_agui() + # always puts RUN_FINISHED last, and the client drops anything that + # arrives after RUN_FINISHED (a finished run is done, full stop) — + # so the approval CustomEvent has to go out as end_custom_event + # (right before RUN_FINISHED), which means draining the raw SDK + # stream ourselves first instead of handing `result` straight to + # to_agui: end_custom_event has to already exist by the time we + # call it, and interruptions aren't known until the drain finishes. + raw_events = [event async for event in result.stream_events()] + + end_custom_event = None + if result.interruptions: + _PENDING_APPROVALS[body.thread_id] = result.to_state() + end_custom_event = CustomEvent( + type=EventType.CUSTOM, + name="approval_request", + value=[ + { + "call_id": getattr(item.raw_item, "call_id", None), + "tool_name": item.tool_name, + "arguments": getattr(item.raw_item, "arguments", None), + } + for item in result.interruptions + ], + ) + + async def _replay(): + for event in raw_events: + yield event + + async for ag_event in _approval_translator.to_agui( + _replay(), body, end_custom_event=end_custom_event + ): + yield _approval_encoder.encode(ag_event) + + return StreamingResponse(_stream(), media_type=_approval_encoder.get_content_type()) + + @app.get("/health") async def health() -> dict: return {"status": "ok", "agents": list(DEMOS)} diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index 39d77f5d35..97ba73a967 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -19,6 +19,9 @@ and right before RUN_FINISHED, via DemoConfig.build_start_custom_event / build_end_custom_event + POST /human_in_the_loop_approval ← backend tool gated by needs_approval, + resumed from result.interruptions + (routed by hand — see below) GET /health ← liveness check @@ -60,7 +63,7 @@ import uvicorn from agents import Runner -from ag_ui.core import RunAgentInput +from ag_ui.core import CustomEvent, EventType, RunAgentInput from ag_ui.encoder import EventEncoder from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse @@ -97,15 +100,105 @@ async def health() -> dict: return {"status": "ok", "agents": list(DEMOS)} +# thread_id -> paused RunState, waiting for an approve/reject decision on the +# next request. In-memory only — fine for a demo process, not a restart or a +# second server instance; a real app would use a session store instead. +_PENDING_APPROVALS: dict[str, object] = {} + + @app.post("/{agent_name}") async def run(agent_name: str, body: RunAgentInput) -> StreamingResponse: """Accept a RunAgentInput, run the named demo agent, stream AG-UI events back via SSE.""" demo = DEMOS.get(agent_name) if demo is None: raise HTTPException(status_code=404, detail=f"Unknown agent: {agent_name!r}") + if agent_name == "human_in_the_loop_approval": + return StreamingResponse( + _stream_approval(demo, body), media_type=encoder.get_content_type() + ) return StreamingResponse(_stream(demo, body), media_type=encoder.get_content_type()) +async def _stream_approval(demo: DemoConfig, body: RunAgentInput): + """Same shape as _stream, plus resuming from result.interruptions. + + Kept separate from _stream rather than adding an if-branch there: this is + the only demo that needs to inspect result.interruptions after the + stream and possibly resume from a stored RunState instead of + translated.messages, and that logic doesn't apply to any other demo. + """ + agent = demo.agent + decision = None + forwarded = body.forwarded_props + if isinstance(forwarded, dict): + decision = forwarded.get("approval") + + pending_state = _PENDING_APPROVALS.pop(body.thread_id, None) + if decision and pending_state is not None: + item = next( + ( + i + for i in pending_state.get_interruptions() + if getattr(i.raw_item, "call_id", None) == decision.get("call_id") + ), + None, + ) + if item is not None: + if decision.get("approve"): + pending_state.approve(item) + else: + pending_state.reject(item) + result = Runner.run_streamed(agent, pending_state) + else: + translated = translator.to_sdk(body) + if translated.tools: + agent = agent.clone(tools=[*agent.tools, *translated.tools]) + result = Runner.run_streamed(agent, input=translated.messages) + + # result.interruptions is only known once the SDK's own stream is fully + # drained — there's no mid-stream event for it. to_agui() always puts + # RUN_FINISHED last, and the client drops anything that arrives after + # RUN_FINISHED, so the approval CustomEvent has to go out as + # end_custom_event (right before RUN_FINISHED) — which means draining + # the raw SDK stream ourselves first instead of handing `result` + # straight to to_agui: end_custom_event has to already exist by the + # time we call it, and interruptions aren't known until the drain + # finishes. + try: + raw_events = [event async for event in result.stream_events()] + except Exception: + logger.exception("Agent run failed") + return + + end_custom_event = None + if result.interruptions: + _PENDING_APPROVALS[body.thread_id] = result.to_state() + end_custom_event = CustomEvent( + type=EventType.CUSTOM, + name="approval_request", + value=[ + { + "call_id": getattr(item.raw_item, "call_id", None), + "tool_name": item.tool_name, + "arguments": getattr(item.raw_item, "arguments", None), + } + for item in result.interruptions + ], + ) + + async def _replay(): + for event in raw_events: + yield event + + try: + async for ag_event in translator.to_agui( + _replay(), body, end_custom_event=end_custom_event + ): + yield encoder.encode(ag_event) + except Exception: + logger.exception("Agent run failed") + + # --------------------------------------------------------------------------- # Core streaming logic — shared by every demo in the registry # --------------------------------------------------------------------------- From 08104a0ae17d800c24a0b96d3f9b87292ec48768 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 02:16:08 +0200 Subject: [PATCH 33/94] feat(logging): change debug logs to warnings for unsupported content types --- .../python/src/ag_ui_openai_agents/engine/agui_to_sdk.py | 3 ++- .../python/src/ag_ui_openai_agents/engine/sdk_to_agui.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py index 4a8288d558..0ef712c979 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py @@ -245,7 +245,7 @@ def translate_message(self, message: Message) -> list[dict[str, Any]]: if isinstance(message, ActivityMessage): item = self.translate_activity_message(message) return [item] if item is not None else [] - logger.debug("Unknown AG-UI message type: %s", type(message).__name__) + logger.warning("Unknown AG-UI message type: %s", type(message).__name__) return [] def translate_user_message(self, message: UserMessage) -> dict[str, Any]: @@ -740,6 +740,7 @@ def _dispatch_dict_content_part( url = self._data_source_to_url(read_attr(part, "source")) return {"type": "input_image", "image_url": url} if url else None # Don't recognize it — skip it rather than guess. + logger.warning("Ignoring unsupported input content type: %s", part_type) return None # -- Helpers for translate_binary_content, split out to keep it readable diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py index 81188c76a5..331895d041 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py @@ -540,7 +540,7 @@ def translate_item(self, item: RunItem) -> list[BaseEvent]: return self.translate_mcp_list_tools_item(item) if isinstance(item, MCPApprovalResponseItem): return self.translate_mcp_approval_response_item(item) - logger.debug("Unknown SDK run item type: %s", type(item).__name__) + logger.warning("Unknown SDK run item type: %s", type(item).__name__) return [] def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEvent]: From 1bcf6c70d7411171cb1b1afd873d5792f35c44ea Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 03:06:50 +0200 Subject: [PATCH 34/94] feat(openai-agents): restructure agents and servers examples - Refactor each OpenAI Agents example into a self-contained FastAPI app and mount all demo apps under a single aggregate server. - Forward AG-UI context and custom lifecycle events through the shared agent wrapper, removing demo-specific routing for dynamic prompts and lifecycle events while preserving the approval-specific resume flow. - Move the OpenAI Agents Python server from port 8022 to 8024 to avoid the AWS Strands TypeScript port conflict. - Update Dojo routing, configuration, documentation, tests, and generated content accordingly. --- apps/dojo/scripts/run-dojo-everything.js | 2 +- apps/dojo/src/agents.ts | 2 +- .../(v2)/custom_lifecycle_events/README.mdx | 8 +- .../(v2)/dynamic_system_prompt/README.mdx | 15 +- .../human_in_the_loop_approval/README.mdx | 14 + apps/dojo/src/env.ts | 2 +- apps/dojo/src/files.json | 38 ++- .../openai-agents/python/examples/README.md | 4 +- .../examples/agents_examples/__init__.py | 25 +- .../examples/agents_examples/agentic_chat.py | 13 +- .../agents_examples/backend_tool_rendering.py | 7 + .../custom_lifecycle_events.py | 21 +- .../agents_examples/dynamic_system_prompt.py | 63 +---- .../agents_examples/human_in_the_loop.py | 7 + .../human_in_the_loop_approval.py | 79 +++++- .../examples/agents_examples/subagents.py | 7 + .../tool_based_generative_ui.py | 9 + .../openai-agents/python/examples/server.py | 251 +++--------------- .../python/examples/translator_server.py | 6 +- .../python/src/ag_ui_openai_agents/agent.py | 21 +- .../openai-agents/python/tests/test_agent.py | 39 ++- 21 files changed, 300 insertions(+), 333 deletions(-) create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/README.mdx diff --git a/apps/dojo/scripts/run-dojo-everything.js b/apps/dojo/scripts/run-dojo-everything.js index 898afb6aa7..951cd1ab69 100755 --- a/apps/dojo/scripts/run-dojo-everything.js +++ b/apps/dojo/scripts/run-dojo-everything.js @@ -225,7 +225,7 @@ const ALL_SERVICES = { name: "OpenAI Agents SDK (Python)", cwd: path.join(integrationsRoot, "openai-agents/python/examples"), env: { - PORT: 8022, + PORT: 8024, OPENAI_API_KEY: process.env.OPENAI_API_KEY || "test-key", ...(!process.env.OPENAI_API_KEY && { OPENAI_BASE_URL: "http://localhost:5555/v1", diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 1597f141f5..6953eead34 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -635,7 +635,7 @@ export const agentsIntegrations = { "openai-agents-python": async () => mapAgents( (path) => - new HttpAgent({ url: `${envVars.openaiAgentsPythonUrl}/${path}` }), + new HttpAgent({ url: `${envVars.openaiAgentsPythonUrl}/${path}/` }), { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx index aae2b000a9..47c0841b52 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx @@ -6,7 +6,7 @@ and `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance (anything else raises `TypeError`), default `None` (off). When set, one `CUSTOM` event is emitted right after `RUN_STARTED`, another right before -`RUN_FINISHED`. This demo's server (`translator_server.py`) uses them to +`RUN_FINISHED`. This demo's `OpenAIAgentsAgent` wrapper forwards them to report fake usage split the way a real bill would be: `input_usage` (prompt tokens/cost — known before the model even runs) at the start, `output_usage` (completion tokens/cost — known only once the run is done) @@ -24,9 +24,9 @@ reported at the end. ## Technical Details - `build_input_usage_event()`/`build_output_usage_event()` - (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s; - `translator_server.py`'s route just calls them and passes the result to - `to_agui(start_custom_event=..., end_custom_event=...)` + (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s + for each run; `OpenAIAgentsAgent` calls the builders and passes the results + to `to_agui(start_custom_event=..., end_custom_event=...)` - Both events are opaque to the translator — it only checks `isinstance(..., CustomEvent)`, the `name`/`value` shape is entirely up to the caller - The page captures the current `runId` from `RUN_STARTED`, associates the diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx index a8dd6091aa..eb9974aeed 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx @@ -10,11 +10,9 @@ bakes "always reply in X" into the system prompt fresh, before each model call. Context needs no `AGUIContext`/state machinery — it's read-only and -regenerated every run, so this demo drives the translator directly instead -of the shared wrapper: `to_sdk()` / `Runner.run_streamed(..., context=...)` -/ `to_agui()`, the same three calls as the other servers, plus the one line -they skip — passing the AG-UI context straight through as the SDK's own -`context=`. +regenerated every run. The shared `OpenAIAgentsAgent` wrapper passes the +translated context into `Runner.run_streamed(..., context=...)`, so this demo +uses the same mounted FastAPI-app pattern as the other examples. ## How to Interact @@ -31,7 +29,6 @@ they skip — passing the AG-UI context straight through as the SDK's own `examples/agents_examples/dynamic_system_prompt.py` reads `ctx.context` (the raw `list[Context]` sent by the client) and returns a fresh instructions string -- Runs as its own standalone FastAPI app (own file, own port) rather than - through the shared demo registry — the shared servers don't forward - `RunAgentInput.context` into `Runner.run_streamed`, so a demo that needs - it runs independently instead of changing that shared loop +- The demo app is mounted by `examples/server.py` at + `/dynamic_system_prompt`; the aggregate OpenAI Agents server runs on port + 8024 diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/README.mdx new file mode 100644 index 0000000000..b4b7d43db9 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/README.mdx @@ -0,0 +1,14 @@ +--- +title: Human in the Loop Approval +description: Pause a backend tool call until a person approves or rejects it. +--- + +## Human in the Loop Approval + +This example uses a backend-owned `issue_refund` tool with the OpenAI Agents +SDK's `needs_approval` option. When the agent asks to issue a refund, the run +pauses and the page shows an approval card. + +Choose **Approve** to execute the refund or **Reject** to resume the agent +without executing it. The server keeps the paused SDK state for the thread and +uses the decision from the next request to continue that same run. diff --git a/apps/dojo/src/env.ts b/apps/dojo/src/env.ts index 4e78a8df35..8df8f72a9c 100644 --- a/apps/dojo/src/env.ts +++ b/apps/dojo/src/env.ts @@ -86,7 +86,7 @@ export default function getEnvVars(): envVars { claudeAgentSdkTypescriptUrl: process.env.CLAUDE_AGENT_SDK_TYPESCRIPT_URL || "http://localhost:8020", openaiAgentsPythonUrl: - process.env.OPENAI_AGENTS_PYTHON_URL || "http://localhost:8022", + process.env.OPENAI_AGENTS_PYTHON_URL || "http://localhost:8024", langroidUrl: process.env.LANGROID_URL || "http://localhost:8021", watsonxRegion: process.env.WATSONX_REGION || "", watsonxInstanceId: process.env.WATSONX_INSTANCE_ID || "", diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index bf23bb01be..32dc2927e6 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4828,7 +4828,7 @@ }, { "name": "agentic_chat.py", - "content": "\"\"\"Agentic chat — plain conversation, no tools.\n\nThe baseline demo: exercises ``TEXT_MESSAGE_START/CONTENT/END`` only. Good\nfirst smoke test before trying the tool / multi-agent examples.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_agentic_chat_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n", + "content": "\"\"\"Agentic chat — plain conversation, no tools.\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_agentic_chat_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\nagent = OpenAIAgentsAgent(create_agentic_chat_agent(), name=\"agentic_chat\")\napp = FastAPI(title=\"Agentic chat AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } @@ -4854,7 +4854,7 @@ }, { "name": "backend_tool_rendering.py", - "content": "\"\"\"Backend tool rendering — a server-side ``@function_tool``.\n\nExercises ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` for a tool the\n*backend* owns and executes (as opposed to :mod:`human_in_the_loop`, where the\nfrontend owns execution). The SDK runs the tool body itself; the translator\njust reports the call and its result as they stream past.\n\nThe dojo's weather card (apps/dojo .../backend_tool_rendering/page.tsx) reads\nthe tool result as a JSON object — temperature/conditions/humidity/\nwind_speed/feels_like — and the argument as `location`, matching every other\nintegration's version of this demo. A plain sentence string or a `city`\nparam renders as a blank/all-zero card, since the frontend has nothing to\nparse. The fixed return value (same for every location) matches how most\nother integrations' versions of this demo work too — the point of the demo\nis the tool-call plumbing, not real weather data.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, function_tool\n\nfrom .constants import DEFAULT_MODEL\n\n_WEATHER = {\"temperature\": 20, \"conditions\": \"sunny\", \"humidity\": 50, \"wind_speed\": 10, \"feels_like\": 20}\n\n\n@function_tool\ndef get_weather(location: str) -> dict:\n \"\"\"Get the current weather for a location.\"\"\"\n return _WEATHER\n\n\ndef create_backend_tool_agent() -> Agent:\n return Agent(\n name=\"weather_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a helpful weather assistant. Use the get_weather tool \"\n \"whenever the user asks about weather in a specific location.\"\n ),\n tools=[get_weather],\n )\n", + "content": "\"\"\"Backend tool rendering — a server-side ``@function_tool``.\n\nExercises ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` for a tool the\n*backend* owns and executes (as opposed to :mod:`human_in_the_loop`, where the\nfrontend owns execution). The SDK runs the tool body itself; the translator\njust reports the call and its result as they stream past.\n\nThe dojo's weather card (apps/dojo .../backend_tool_rendering/page.tsx) reads\nthe tool result as a JSON object — temperature/conditions/humidity/\nwind_speed/feels_like — and the argument as `location`, matching every other\nintegration's version of this demo. A plain sentence string or a `city`\nparam renders as a blank/all-zero card, since the frontend has nothing to\nparse. The fixed return value (same for every location) matches how most\nother integrations' versions of this demo work too — the point of the demo\nis the tool-call plumbing, not real weather data.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, function_tool\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n_WEATHER = {\"temperature\": 20, \"conditions\": \"sunny\", \"humidity\": 50, \"wind_speed\": 10, \"feels_like\": 20}\n\n\n@function_tool\ndef get_weather(location: str) -> dict:\n \"\"\"Get the current weather for a location.\"\"\"\n return _WEATHER\n\n\ndef create_backend_tool_agent() -> Agent:\n return Agent(\n name=\"weather_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a helpful weather assistant. Use the get_weather tool \"\n \"whenever the user asks about weather in a specific location.\"\n ),\n tools=[get_weather],\n )\n\n\nagent = OpenAIAgentsAgent(create_backend_tool_agent(), name=\"backend_tool_rendering\")\napp = FastAPI(title=\"Backend tool rendering AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } @@ -4874,7 +4874,27 @@ }, { "name": "human_in_the_loop.py", - "content": "\"\"\"Human-in-the-loop — a *frontend*-owned tool, approved before execution.\n\nUnlike :mod:`backend_tool_rendering`, ``generate_task_steps`` has no server\nimplementation — it arrives per-request as an AG-UI client tool\n(``RunAgentInput.tools``), gets wrapped into an SDK ``FunctionTool`` proxy by\n``AGUIToSDKTranslator.translate_tools()``, and is merged onto this agent by\nthe run loop (see ``server.py``).\n\n``tool_use_behavior=StopAtTools(...)`` is the SDK's built-in for \"the model\nmay only *call* this tool, never execute it\": the run ends the moment the\nmodel emits the call, before the (dead-code) proxy body would ever run. The\nfrontend renders the steps for user approval, then sends the result back as\nan AG-UI ``ToolMessage`` in the *next* request — ordinary multi-turn history,\nno custom pause/resume machinery needed.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\n\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_task_steps\"\n\nINSTRUCTIONS = \"\"\"You are a task planning assistant that breaks work into clear, actionable steps.\n\nWhen the user asks for help with a task:\n1. Immediately call the `generate_task_steps` tool with an array of steps.\n Each step is an object: {\"description\": \"...\", \"status\": \"enabled\"}.\n2. Do not restate the steps as plain text — the frontend renders them.\n3. After the call, wait for the user to approve/select steps; do not call\n the tool again until they respond.\n\"\"\"\n\n\ndef create_human_in_the_loop_agent() -> Agent:\n return Agent(\n name=\"task_planner\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n", + "content": "\"\"\"Human-in-the-loop — a *frontend*-owned tool, approved before execution.\n\nUnlike :mod:`backend_tool_rendering`, ``generate_task_steps`` has no server\nimplementation — it arrives per-request as an AG-UI client tool\n(``RunAgentInput.tools``), gets wrapped into an SDK ``FunctionTool`` proxy by\n``AGUIToSDKTranslator.translate_tools()``, and is merged onto this agent by\nthe run loop (see ``server.py``).\n\n``tool_use_behavior=StopAtTools(...)`` is the SDK's built-in for \"the model\nmay only *call* this tool, never execute it\": the run ends the moment the\nmodel emits the call, before the (dead-code) proxy body would ever run. The\nfrontend renders the steps for user approval, then sends the result back as\nan AG-UI ``ToolMessage`` in the *next* request — ordinary multi-turn history,\nno custom pause/resume machinery needed.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_task_steps\"\n\nINSTRUCTIONS = \"\"\"You are a task planning assistant that breaks work into clear, actionable steps.\n\nWhen the user asks for help with a task:\n1. Immediately call the `generate_task_steps` tool with an array of steps.\n Each step is an object: {\"description\": \"...\", \"status\": \"enabled\"}.\n2. Do not restate the steps as plain text — the frontend renders them.\n3. After the call, wait for the user to approve/select steps; do not call\n the tool again until they respond.\n\"\"\"\n\n\ndef create_human_in_the_loop_agent() -> Agent:\n return Agent(\n name=\"task_planner\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n\n\nagent = OpenAIAgentsAgent(create_human_in_the_loop_agent(), name=\"human_in_the_loop\")\napp = FastAPI(title=\"Human in the loop AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "language": "python", + "type": "file" + } + ], + "openai-agents-python::human_in_the_loop_approval": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useCopilotKit,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface ApprovalProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface PendingApproval {\n callId: string;\n toolName: string;\n arguments: string;\n}\n\nconst Approval: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"human_in_the_loop_approval\" });\n const { copilotkit } = useCopilotKit();\n const [pending, setPending] = useState(null);\n const [resolvedCallId, setResolvedCallId] = useState(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Refund an order\", message: \"I'd like a refund for ORD-1001.\" },\n { title: \"Refund another order\", message: \"Please refund ORD-1002.\" },\n { title: \"Unknown order\", message: \"Can you refund ORD-9999?\" },\n ],\n available: \"always\",\n consumerAgentId: \"human_in_the_loop_approval\",\n });\n\n React.useEffect(() => {\n const subscription = agent.subscribe({\n onCustomEvent: ({ event }) => {\n if (event.name !== \"approval_request\") return;\n // One CustomEvent can carry several pending calls if the model made\n // more than one needs_approval call in a turn; this demo only ever\n // triggers one, so showing the first is enough here.\n const pendingList = event.value as Array<{\n call_id: string;\n tool_name: string;\n arguments: string;\n }>;\n const first = pendingList[0];\n if (!first) return;\n setResolvedCallId(null);\n setPending({\n callId: first.call_id,\n toolName: first.tool_name,\n arguments: first.arguments,\n });\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n const respond = (approve: boolean) => {\n if (!pending) return;\n setResolvedCallId(pending.callId);\n copilotkit.runAgent({\n agent,\n forwardedProps: {\n approval: { call_id: pending.callId, approve },\n },\n });\n setPending(null);\n };\n\n return (\n
\n {pending && (\n respond(true)} onReject={() => respond(false)} />\n )}\n {!pending && resolvedCallId && (\n
Decision sent — waiting for the agent...
\n )}\n
\n \n
\n
\n );\n};\n\nfunction ApprovalCard({\n pending,\n onApprove,\n onReject,\n}: {\n pending: PendingApproval;\n onApprove: () => void;\n onReject: () => void;\n}) {\n let args: Record = {};\n try {\n args = JSON.parse(pending.arguments);\n } catch {\n // leave args empty — raw string shown below is enough context either way\n }\n\n return (\n \n

Approval needed

\n

\n The agent wants to call {pending.toolName} with:\n

\n
\n        {JSON.stringify(args, null, 2)}\n      
\n
\n \n Reject\n \n \n Approve\n \n
\n \n );\n}\n\nexport default Approval;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "---\ntitle: Human in the Loop Approval\ndescription: Pause a backend tool call until a person approves or rejects it.\n---\n\n## Human in the Loop Approval\n\nThis example uses a backend-owned `issue_refund` tool with the OpenAI Agents\nSDK's `needs_approval` option. When the agent asks to issue a refund, the run\npauses and the page shows an approval card.\n\nChoose **Approve** to execute the refund or **Reject** to resume the agent\nwithout executing it. The server keeps the paused SDK state for the thread and\nuses the decision from the next request to continue that same run.\n", + "language": "markdown", + "type": "file" + }, + { + "name": "human_in_the_loop_approval.py", + "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, Runner, function_tool\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n async def stream():\n decision = None\n if isinstance(body.forwarded_props, dict):\n decision = body.forwarded_props.get(\"approval\")\n\n pending_state = _pending_approvals.pop(body.thread_id, None)\n if decision and pending_state is not None:\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is not None:\n if decision.get(\"approve\"):\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_sdk(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", "language": "python", "type": "file" } @@ -4900,7 +4920,7 @@ }, { "name": "tool_based_generative_ui.py", - "content": "\"\"\"Tool-based generative UI — frontend tool renders the content.\n\nLike :mod:`human_in_the_loop`, the tool (``generate_haiku``) is *client*-owned:\nit arrives per-request in ``RunAgentInput.tools``, gets wrapped into an SDK\n``FunctionTool`` proxy, and ``StopAtTools`` ends the run the moment the model\ncalls it. The difference is intent: here the tool call *is* the deliverable —\nthe frontend renders the haiku card from the streamed ``TOOL_CALL_ARGS``,\nno approval round-trip expected.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\n\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_haiku\"\n\nINSTRUCTIONS = \"\"\"You are a creative writing assistant that renders haikus with a UI component.\n\nWhen the user asks for a haiku:\n1. Immediately call the `generate_haiku` tool with the haiku data\n (Japanese lines, English lines, and any other fields the tool declares).\n2. Do NOT write the haiku as plain text — the frontend renders it.\n\nFor non-creative requests, respond normally without the tool.\n\"\"\"\n\n\ndef create_tool_based_generative_ui_agent() -> Agent:\n return Agent(\n name=\"haiku_assistant\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n", + "content": "\"\"\"Tool-based generative UI — frontend tool renders the content.\n\nLike :mod:`human_in_the_loop`, the tool (``generate_haiku``) is *client*-owned:\nit arrives per-request in ``RunAgentInput.tools``, gets wrapped into an SDK\n``FunctionTool`` proxy, and ``StopAtTools`` ends the run the moment the model\ncalls it. The difference is intent: here the tool call *is* the deliverable —\nthe frontend renders the haiku card from the streamed ``TOOL_CALL_ARGS``,\nno approval round-trip expected.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_haiku\"\n\nINSTRUCTIONS = \"\"\"You are a creative writing assistant that renders haikus with a UI component.\n\nWhen the user asks for a haiku:\n1. Immediately call the `generate_haiku` tool with the haiku data\n (Japanese lines, English lines, and any other fields the tool declares).\n2. Do NOT write the haiku as plain text — the frontend renders it.\n\nFor non-creative requests, respond normally without the tool.\n\"\"\"\n\n\ndef create_tool_based_generative_ui_agent() -> Agent:\n return Agent(\n name=\"haiku_assistant\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n\n\nagent = OpenAIAgentsAgent(\n create_tool_based_generative_ui_agent(), name=\"tool_based_generative_ui\"\n)\napp = FastAPI(title=\"Tool-based generative UI AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } @@ -4920,7 +4940,7 @@ }, { "name": "subagents.py", - "content": "\"\"\"Subagents — multi-agent via the SDK's agents-as-tools pattern.\n\nUnlike a handoff (control *transfers* to the specialist, triage agent exits\nthe conversation), the supervisor here stays in charge and *calls*\nspecialists as tools (``Agent.as_tool()``), possibly several in one turn,\nthen synthesizes their outputs itself — a call-and-return delegation, not a\nhandoff. Same shape as the LangGraph \"subagents\" showcase demo (a supervisor\ncalling child agents as tools and getting results back), just built with the\nSDK's own ``as_tool()`` instead of a routing graph.\n\nOn the AG-UI side each specialist invocation is an ordinary\n``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested\nagent's own model turns stay internal to the SDK and are not streamed as\nseparate messages, so the client sees one coherent supervisor transcript.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\nresearch_agent = Agent(\n name=\"research_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You research a topic and return the key facts as a short bullet \"\n \"list. Facts only — no fluff, no conclusions.\"\n ),\n)\n\nwriter_agent = Agent(\n name=\"writer_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You turn bullet-point facts into short, engaging prose. One tight \"\n \"paragraph unless asked otherwise.\"\n ),\n)\n\ncritic_agent = Agent(\n name=\"critic_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You review a draft and return concrete improvement suggestions as a \"\n \"numbered list. Max 3 suggestions, be specific.\"\n ),\n)\n\n\ndef create_subagents_agent() -> Agent:\n return Agent(\n name=\"orchestrator\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You orchestrate a small content team. For writing requests: \"\n \"call research_topic for the facts, then write_prose to draft, \"\n \"then critique_draft to review, then produce the final version \"\n \"yourself incorporating the critique. For simple questions, \"\n \"answer directly without the team.\"\n ),\n tools=[\n research_agent.as_tool(\n tool_name=\"research_topic\",\n tool_description=\"Research a topic and return key facts as bullets.\",\n ),\n writer_agent.as_tool(\n tool_name=\"write_prose\",\n tool_description=\"Turn bullet-point facts into short prose.\",\n ),\n critic_agent.as_tool(\n tool_name=\"critique_draft\",\n tool_description=\"Review a draft and suggest up to 3 improvements.\",\n ),\n ],\n )\n", + "content": "\"\"\"Subagents — multi-agent via the SDK's agents-as-tools pattern.\n\nUnlike a handoff (control *transfers* to the specialist, triage agent exits\nthe conversation), the supervisor here stays in charge and *calls*\nspecialists as tools (``Agent.as_tool()``), possibly several in one turn,\nthen synthesizes their outputs itself — a call-and-return delegation, not a\nhandoff. Same shape as the LangGraph \"subagents\" showcase demo (a supervisor\ncalling child agents as tools and getting results back), just built with the\nSDK's own ``as_tool()`` instead of a routing graph.\n\nOn the AG-UI side each specialist invocation is an ordinary\n``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested\nagent's own model turns stay internal to the SDK and are not streamed as\nseparate messages, so the client sees one coherent supervisor transcript.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\nresearch_agent = Agent(\n name=\"research_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You research a topic and return the key facts as a short bullet \"\n \"list. Facts only — no fluff, no conclusions.\"\n ),\n)\n\nwriter_agent = Agent(\n name=\"writer_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You turn bullet-point facts into short, engaging prose. One tight \"\n \"paragraph unless asked otherwise.\"\n ),\n)\n\ncritic_agent = Agent(\n name=\"critic_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You review a draft and return concrete improvement suggestions as a \"\n \"numbered list. Max 3 suggestions, be specific.\"\n ),\n)\n\n\ndef create_subagents_agent() -> Agent:\n return Agent(\n name=\"orchestrator\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You orchestrate a small content team. For writing requests: \"\n \"call research_topic for the facts, then write_prose to draft, \"\n \"then critique_draft to review, then produce the final version \"\n \"yourself incorporating the critique. For simple questions, \"\n \"answer directly without the team.\"\n ),\n tools=[\n research_agent.as_tool(\n tool_name=\"research_topic\",\n tool_description=\"Research a topic and return key facts as bullets.\",\n ),\n writer_agent.as_tool(\n tool_name=\"write_prose\",\n tool_description=\"Turn bullet-point facts into short prose.\",\n ),\n critic_agent.as_tool(\n tool_name=\"critique_draft\",\n tool_description=\"Review a draft and suggest up to 3 improvements.\",\n ),\n ],\n )\n\n\nagent = OpenAIAgentsAgent(create_subagents_agent(), name=\"subagents\")\napp = FastAPI(title=\"Subagents AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } @@ -4934,13 +4954,13 @@ }, { "name": "README.mdx", - "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes two optional params — `start_custom_event`\nand `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance\n(anything else raises `TypeError`), default `None` (off). When set, one\n`CUSTOM` event is emitted right after `RUN_STARTED`, another right before\n`RUN_FINISHED`. This demo's server (`translator_server.py`) uses them to\nreport fake usage split the way a real bill would be: `input_usage`\n(prompt tokens/cost — known before the model even runs) at the start,\n`output_usage` (completion tokens/cost — known only once the run is done)\nat the end — plain conversation otherwise, same agent as `agentic_chat`.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about AI.\"\n\nEach assistant reply has a compact usage label immediately above it. The label\nshows the input usage reported at the beginning of the run and the output usage\nreported at the end.\n\n## Technical Details\n\n- `build_input_usage_event()`/`build_output_usage_event()`\n (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s;\n `translator_server.py`'s route just calls them and passes the result to\n `to_agui(start_custom_event=..., end_custom_event=...)`\n- Both events are opaque to the translator — it only checks `isinstance(...,\n CustomEvent)`, the `name`/`value` shape is entirely up to the caller\n- The page captures the current `runId` from `RUN_STARTED`, associates the\n custom usage events with that run in a small external store, then uses\n `CopilotKitProvider`'s stable `renderCustomMessages` configuration to render\n the matching label above the assistant message\n- The token/cost numbers are fake — the point is the event bracketing, not\n real usage accounting\n", + "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes two optional params — `start_custom_event`\nand `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance\n(anything else raises `TypeError`), default `None` (off). When set, one\n`CUSTOM` event is emitted right after `RUN_STARTED`, another right before\n`RUN_FINISHED`. This demo's `OpenAIAgentsAgent` wrapper forwards them to\nreport fake usage split the way a real bill would be: `input_usage`\n(prompt tokens/cost — known before the model even runs) at the start,\n`output_usage` (completion tokens/cost — known only once the run is done)\nat the end — plain conversation otherwise, same agent as `agentic_chat`.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about AI.\"\n\nEach assistant reply has a compact usage label immediately above it. The label\nshows the input usage reported at the beginning of the run and the output usage\nreported at the end.\n\n## Technical Details\n\n- `build_input_usage_event()`/`build_output_usage_event()`\n (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s\n for each run; `OpenAIAgentsAgent` calls the builders and passes the results\n to `to_agui(start_custom_event=..., end_custom_event=...)`\n- Both events are opaque to the translator — it only checks `isinstance(...,\n CustomEvent)`, the `name`/`value` shape is entirely up to the caller\n- The page captures the current `runId` from `RUN_STARTED`, associates the\n custom usage events with that run in a small external store, then uses\n `CopilotKitProvider`'s stable `renderCustomMessages` configuration to render\n the matching label above the assistant message\n- The token/cost numbers are fake — the point is the event bracketing, not\n real usage accounting\n", "language": "markdown", "type": "file" }, { "name": "custom_lifecycle_events.py", - "content": "\"\"\"Custom lifecycle events — manual to_agui() with a CUSTOM event bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. This ``DemoConfig`` sets\n``build_start_custom_event``/``build_end_custom_event`` (see\n``agents_examples/__init__.py``); the shared run loop in\ntranslator_server.py calls them and forwards the result into\n``to_agui()``'s ``start_custom_event``/``end_custom_event`` params, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom agents import Agent\n\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_event() -> CustomEvent:\n tokens = random.randint(20, 120)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"input_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)},\n )\n\n\ndef build_output_usage_event() -> CustomEvent:\n tokens = random.randint(120, 480)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"output_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)},\n )\n", + "content": "\"\"\"Custom lifecycle events — CUSTOM events bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. This ``DemoConfig`` sets\n``build_start_custom_event``/``build_end_custom_event``. The wrapper forwards\ntheir results to ``to_agui()``'s ``start_custom_event``/``end_custom_event``\nparams, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_event() -> CustomEvent:\n tokens = random.randint(20, 120)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"input_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)},\n )\n\n\ndef build_output_usage_event() -> CustomEvent:\n tokens = random.randint(120, 480)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"output_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)},\n )\n\n\nagent = OpenAIAgentsAgent(\n create_custom_lifecycle_events_agent(),\n name=\"custom_lifecycle_events\",\n build_start_custom_event=build_input_usage_event,\n build_end_custom_event=build_output_usage_event,\n)\napp = FastAPI(title=\"Custom lifecycle events AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } @@ -4954,13 +4974,13 @@ }, { "name": "README.mdx", - "content": "# 🌐 Dynamic System Prompt\n\n## What This Demo Shows\n\nThe frontend sends a language choice over the AG-UI **context** channel — a\nplain `{description, value}` pair, no special class involved. The agent's\n`instructions` is a callable (`Agent.instructions`, the OpenAI Agents SDK's\nown dynamic-instructions feature) that reads the context list every turn and\nbakes \"always reply in X\" into the system prompt fresh, before each model\ncall.\n\nContext needs no `AGUIContext`/state machinery — it's read-only and\nregenerated every run, so this demo drives the translator directly instead\nof the shared wrapper: `to_sdk()` / `Runner.run_streamed(..., context=...)`\n/ `to_agui()`, the same three calls as the other servers, plus the one line\nthey skip — passing the AG-UI context straight through as the SDK's own\n`context=`.\n\n## How to Interact\n\n- Pick a language in the sidebar, then ask anything — the reply comes back\n entirely in that language, regardless of what language you type in.\n- Switch languages mid-conversation and ask again — the next reply follows\n the new choice immediately, no restart needed.\n\n## Technical Details\n\n- `useAgentContext({ description: \"Reply language\", value: language })` —\n pushes the current pick onto `RunAgentInput.context` with every message\n- `dynamic_instructions(ctx, agent)` in\n `examples/agents_examples/dynamic_system_prompt.py` reads\n `ctx.context` (the raw `list[Context]` sent by the client) and returns a\n fresh instructions string\n- Runs as its own standalone FastAPI app (own file, own port) rather than\n through the shared demo registry — the shared servers don't forward\n `RunAgentInput.context` into `Runner.run_streamed`, so a demo that needs\n it runs independently instead of changing that shared loop\n", + "content": "# 🌐 Dynamic System Prompt\n\n## What This Demo Shows\n\nThe frontend sends a language choice over the AG-UI **context** channel — a\nplain `{description, value}` pair, no special class involved. The agent's\n`instructions` is a callable (`Agent.instructions`, the OpenAI Agents SDK's\nown dynamic-instructions feature) that reads the context list every turn and\nbakes \"always reply in X\" into the system prompt fresh, before each model\ncall.\n\nContext needs no `AGUIContext`/state machinery — it's read-only and\nregenerated every run. The shared `OpenAIAgentsAgent` wrapper passes the\ntranslated context into `Runner.run_streamed(..., context=...)`, so this demo\nuses the same mounted FastAPI-app pattern as the other examples.\n\n## How to Interact\n\n- Pick a language in the sidebar, then ask anything — the reply comes back\n entirely in that language, regardless of what language you type in.\n- Switch languages mid-conversation and ask again — the next reply follows\n the new choice immediately, no restart needed.\n\n## Technical Details\n\n- `useAgentContext({ description: \"Reply language\", value: language })` —\n pushes the current pick onto `RunAgentInput.context` with every message\n- `dynamic_instructions(ctx, agent)` in\n `examples/agents_examples/dynamic_system_prompt.py` reads\n `ctx.context` (the raw `list[Context]` sent by the client) and returns a\n fresh instructions string\n- The demo app is mounted by `examples/server.py` at\n `/dynamic_system_prompt`; the aggregate OpenAI Agents server runs on port\n 8024\n", "language": "markdown", "type": "file" }, { "name": "dynamic_system_prompt.py", - "content": "\"\"\"Dynamic system prompt — reply language driven by the AG-UI ``context`` channel.\n\nThe point of this demo: **context needs no special AG-UI class.** The frontend\nsends a language choice (English / Arabic / German) over the AG-UI ``context``\nchannel — a plain ``list[Context]`` of ``{description, value}`` items on\n``RunAgentInput.context``. This example folds it into the system prompt with the\nOpenAI Agents SDK's own native feature: ``Agent.instructions`` accepting a\ncallable ``(RunContextWrapper, Agent) -> str``, re-run every turn.\n\nStandalone file, own FastAPI app: neither ``server.py`` nor\n``translator_server.py``'s shared run loops forward ``RunAgentInput.context``\ninto ``Runner.run_streamed`` (``to_sdk()`` hands it back on\n``TranslatedInput.context``, but nothing in those two loops passes it on —\nby design, per ``engine/agui_to_sdk.py``'s own docstring: \"Nothing folds these\ninto the prompt for you\"). Rather than change either shared loop for one demo,\nthis file runs itself: same three calls (``to_sdk`` / ``Runner.run_streamed``\n/ ``to_agui``) as those servers, plus the one line they skip —\n``context=translated.context`` — passed straight through as the SDK's own\n``context=`` and read back via ``RunContextWrapper.context`` in\n``dynamic_instructions``. No ``AGUIContext`` involved anywhere: this demo has\nno state, only context, and context is just a list to read.\n\nRun:\n OPENAI_API_KEY=sk-... uv run python -m agents_examples.dynamic_system_prompt\n\nTest:\n curl -N -X POST http://localhost:8024/dynamic_system_prompt \\\\\n -H 'Content-Type: application/json' \\\\\n -d '{\n \"thread_id\": \"t1\", \"run_id\": \"r1\",\n \"messages\": [{\"id\":\"m1\",\"role\":\"user\",\"content\":\"How is the weather?\"}],\n \"tools\": [], \"state\": {},\n \"context\": [{\"description\": \"Reply language\", \"value\": \"Arabic\"}],\n \"forwarded_props\": null\n }'\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport os\n\nimport uvicorn\nfrom agents import Agent, Runner, RunContextWrapper\nfrom ag_ui.core import Context, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui_openai_agents import AGUITranslator\n\nfrom .constants import DEFAULT_MODEL\n\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\nBASE_INSTRUCTIONS = (\n \"You are a helpful, concise assistant. Answer the user's questions directly.\"\n)\n\n# Fallback when the frontend hasn't picked a language yet.\nDEFAULT_LANGUAGE = \"English\"\n\n\ndef _read_language(ctx: RunContextWrapper[list[Context]]) -> str:\n \"\"\"Pull the reply language out of the AG-UI context list.\n\n ``ctx.context`` here IS the raw ``list[Context]`` the client sent —\n each item a ``{description, value}`` pair, nothing wrapping it. We match\n the item whose description mentions \"language\" and use its value.\n \"\"\"\n items = ctx.context or []\n for item in items:\n if \"language\" in (item.description or \"\").lower():\n return item.value or DEFAULT_LANGUAGE\n return DEFAULT_LANGUAGE\n\n\ndef dynamic_instructions(ctx: RunContextWrapper[list[Context]], agent: Agent) -> str:\n \"\"\"Native SDK dynamic-instructions hook: build the prompt fresh each turn,\n baking in whatever language the frontend currently has selected.\"\"\"\n language = _read_language(ctx)\n return (\n f\"{BASE_INSTRUCTIONS}\\n\"\n f\"Always reply in {language}, no matter what language the user writes in. \"\n f\"Every word of your response must be in {language}.\"\n )\n\n\ndef create_dynamic_system_prompt_agent() -> Agent:\n return Agent(\n name=\"multilingual_assistant\",\n model=DEFAULT_MODEL,\n instructions=dynamic_instructions,\n )\n\n\nagent = create_dynamic_system_prompt_agent()\ntranslator = AGUITranslator()\nencoder = EventEncoder()\n\napp = FastAPI(title=\"AG-UI × OpenAI Agents SDK — dynamic system prompt\")\n\n\n@app.get(\"/health\")\nasync def health() -> dict:\n return {\"status\": \"ok\", \"agent\": agent.name}\n\n\n@app.post(\"/dynamic_system_prompt\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n return StreamingResponse(_stream(body), media_type=encoder.get_content_type())\n\n\nasync def _stream(body: RunAgentInput):\n translated = translator.to_sdk(body)\n\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n\n try:\n result = Runner.run_streamed(\n run_agent,\n input=translated.messages,\n context=translated.context,\n )\n async for ag_event in translator.to_agui(result, body):\n yield encoder.encode(ag_event)\n except Exception:\n logger.exception(\"Agent run failed\")\n\n\ndef main() -> int:\n if not os.getenv(\"OPENAI_API_KEY\"):\n print(\"Error: OPENAI_API_KEY required\")\n return 1\n port = int(os.getenv(\"PORT\", \"8024\"))\n host = os.getenv(\"HOST\", \"0.0.0.0\")\n print(f\"Starting dynamic_system_prompt server on port {port}\")\n uvicorn.run(app, host=host, port=port, log_level=\"info\")\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n", + "content": "\"\"\"Dynamic system prompt — reply language driven by the AG-UI ``context`` channel.\n\nThe frontend sends a language choice in ``RunAgentInput.context`` and the SDK\nrebuilds its instructions for that request.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, RunContextWrapper\nfrom ag_ui.core import Context\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\nBASE_INSTRUCTIONS = (\n \"You are a helpful, concise assistant. Answer the user's questions directly.\"\n)\n\n# Fallback when the frontend hasn't picked a language yet.\nDEFAULT_LANGUAGE = \"English\"\n\n\ndef _read_language(ctx: RunContextWrapper[list[Context]]) -> str:\n \"\"\"Pull the reply language out of the AG-UI context list.\n\n ``ctx.context`` here IS the raw ``list[Context]`` the client sent —\n each item a ``{description, value}`` pair, nothing wrapping it. We match\n the item whose description mentions \"language\" and use its value.\n \"\"\"\n items = ctx.context or []\n for item in items:\n if \"language\" in (item.description or \"\").lower():\n return item.value or DEFAULT_LANGUAGE\n return DEFAULT_LANGUAGE\n\n\ndef dynamic_instructions(ctx: RunContextWrapper[list[Context]], agent: Agent) -> str:\n \"\"\"Native SDK dynamic-instructions hook: build the prompt fresh each turn,\n baking in whatever language the frontend currently has selected.\"\"\"\n language = _read_language(ctx)\n return (\n f\"{BASE_INSTRUCTIONS}\\n\"\n f\"Always reply in {language}, no matter what language the user writes in. \"\n f\"Every word of your response must be in {language}.\"\n )\n\n\ndef create_dynamic_system_prompt_agent() -> Agent:\n return Agent(\n name=\"multilingual_assistant\",\n model=DEFAULT_MODEL,\n instructions=dynamic_instructions,\n )\n\n\nagent = OpenAIAgentsAgent(create_dynamic_system_prompt_agent(), name=\"dynamic_system_prompt\")\napp = FastAPI(title=\"Dynamic system prompt AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 9603c1f5ae..e51bd98e35 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -28,13 +28,13 @@ cp .env.example .env # fill in OPENAI_API_KEY uv run python server.py ``` -Server runs on **http://localhost:8022** (the port the AG-UI Dojo expects; +Server runs on **http://localhost:8024** (the port the AG-UI Dojo expects; override with `PORT`). ## Testing ```bash -curl -N -X POST http://localhost:8022/agentic_chat \ +curl -N -X POST http://localhost:8024/agentic_chat \ -H 'Content-Type: application/json' \ -d '{ "thread_id": "t1", diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py index c8ed885797..f27a2ecae4 100644 --- a/integrations/openai-agents/python/examples/agents_examples/__init__.py +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -1,25 +1,15 @@ """Example agent factories for the AG-UI × OpenAI Agents SDK server. Each demo is a ``DemoConfig`` wrapping the SDK agent. ``build_registry()`` -assembles the full name → config map both servers serve, one route per key. +assembles the full name → config map the aggregate servers serve, one route per key. -Most demos only set ``agent`` — translator_server.py's shared run loop calls -plain ``to_agui(result, body)`` for those. custom_lifecycle_events also sets +Most demos only set ``agent``. custom_lifecycle_events also sets ``build_start_custom_event``/``build_end_custom_event``: optional callables returning a ``CustomEvent``, which the shared loop forwards into ``to_agui(..., start_custom_event=..., end_custom_event=...)`` when present. No if-branch keyed by demo name, no separate router — one generic loop, one optional per-demo hook. -dynamic_system_prompt is registered here too (so DEMOS/health lists it) but -is never run through the generic loop or wrapper — it needs -``context=run_input.context`` on ``Runner.run_streamed``, which neither the -wrapper (``OpenAIAgentsAgent.run()``) nor the shared translator loops pass. -Its own run loop lives next to the agent in -``dynamic_system_prompt.py`` (``stream()``); ``server.py`` and -``.dev/groq_server.py`` both call that function directly from a hand-written -route instead. - Stateful demos (shared_state, agentic_generative_ui, predictive_state_updates) are shelved together with ``AGUIContext`` — see ``.dev/shelved/``. @@ -40,7 +30,7 @@ build_output_usage_event, create_custom_lifecycle_events_agent, ) -from .dynamic_system_prompt import agent as dynamic_system_prompt_agent +from .dynamic_system_prompt import create_dynamic_system_prompt_agent from .human_in_the_loop import create_human_in_the_loop_agent from .human_in_the_loop_approval import create_human_in_the_loop_approval_agent from .subagents import create_subagents_agent @@ -65,8 +55,7 @@ def build_registry() -> dict[str, DemoConfig]: # Same idea as human_in_the_loop (pause for a person before an action # happens) but a different mechanism — see # human_in_the_loop_approval.py's docstring for the frontend-tool vs - # SDK-native-approval distinction. Hand-routed, same reason as - # dynamic_system_prompt below. + # SDK-native-approval distinction. "human_in_the_loop_approval": DemoConfig(agent=create_human_in_the_loop_approval_agent()), "tool_based_generative_ui": DemoConfig(agent=create_tool_based_generative_ui_agent()), "subagents": DemoConfig(agent=create_subagents_agent()), @@ -75,11 +64,7 @@ def build_registry() -> dict[str, DemoConfig]: build_start_custom_event=build_input_usage_event, build_end_custom_event=build_output_usage_event, ), - # Same agent singleton dynamic_system_prompt.stream() runs — only - # registered here so DEMOS/health lists it. Servers route this one by - # hand (dynamic_system_prompt.stream) instead of through this agent - # generically, since it needs context= the generic loop can't give it. - "dynamic_system_prompt": DemoConfig(agent=dynamic_system_prompt_agent), + "dynamic_system_prompt": DemoConfig(agent=create_dynamic_system_prompt_agent()), } diff --git a/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py b/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py index 7e783ddefd..b688e0a366 100644 --- a/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py +++ b/integrations/openai-agents/python/examples/agents_examples/agentic_chat.py @@ -1,13 +1,11 @@ -"""Agentic chat — plain conversation, no tools. - -The baseline demo: exercises ``TEXT_MESSAGE_START/CONTENT/END`` only. Good -first smoke test before trying the tool / multi-agent examples. -""" +"""Agentic chat — plain conversation, no tools.""" from __future__ import annotations from agents import Agent +from fastapi import FastAPI +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint from .constants import DEFAULT_MODEL @@ -17,3 +15,8 @@ def create_agentic_chat_agent() -> Agent: model=DEFAULT_MODEL, instructions="You are a helpful assistant. Be concise.", ) + + +agent = OpenAIAgentsAgent(create_agentic_chat_agent(), name="agentic_chat") +app = FastAPI(title="Agentic chat AG-UI demo") +add_openai_agents_fastapi_endpoint(app, agent, "/") diff --git a/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py b/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py index 0ad85abca9..eabc2ceb56 100644 --- a/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py +++ b/integrations/openai-agents/python/examples/agents_examples/backend_tool_rendering.py @@ -18,7 +18,9 @@ from __future__ import annotations from agents import Agent, function_tool +from fastapi import FastAPI +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint from .constants import DEFAULT_MODEL _WEATHER = {"temperature": 20, "conditions": "sunny", "humidity": 50, "wind_speed": 10, "feels_like": 20} @@ -40,3 +42,8 @@ def create_backend_tool_agent() -> Agent: ), tools=[get_weather], ) + + +agent = OpenAIAgentsAgent(create_backend_tool_agent(), name="backend_tool_rendering") +app = FastAPI(title="Backend tool rendering AG-UI demo") +add_openai_agents_fastapi_endpoint(app, agent, "/") diff --git a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py index 0b68ddfe41..be1784f638 100644 --- a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py +++ b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py @@ -1,11 +1,10 @@ -"""Custom lifecycle events — manual to_agui() with a CUSTOM event bracketing the run. +"""Custom lifecycle events — CUSTOM events bracketing the run. Plain chat agent, same as agentic_chat — the point isn't the agent, it's the two builder functions below. This ``DemoConfig`` sets -``build_start_custom_event``/``build_end_custom_event`` (see -``agents_examples/__init__.py``); the shared run loop in -translator_server.py calls them and forwards the result into -``to_agui()``'s ``start_custom_event``/``end_custom_event`` params, so one +``build_start_custom_event``/``build_end_custom_event``. The wrapper forwards +their results to ``to_agui()``'s ``start_custom_event``/``end_custom_event`` +params, so one CUSTOM event goes out right after RUN_STARTED and another right before RUN_FINISHED. Only CustomEvent instances are accepted there; anything else raises TypeError. @@ -22,7 +21,9 @@ from ag_ui.core import CustomEvent, EventType from agents import Agent +from fastapi import FastAPI +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint from .constants import DEFAULT_MODEL @@ -50,3 +51,13 @@ def build_output_usage_event() -> CustomEvent: name="output_usage", value={"tokens": tokens, "cost_usd": round(tokens * 0.0000006, 6)}, ) + + +agent = OpenAIAgentsAgent( + create_custom_lifecycle_events_agent(), + name="custom_lifecycle_events", + build_start_custom_event=build_input_usage_event, + build_end_custom_event=build_output_usage_event, +) +app = FastAPI(title="Custom lifecycle events AG-UI demo") +add_openai_agents_fastapi_endpoint(app, agent, "/") diff --git a/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py b/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py index 12bf9d3c22..5cfb2c875c 100644 --- a/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py +++ b/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py @@ -1,37 +1,16 @@ """Dynamic system prompt — reply language driven by the AG-UI ``context`` channel. -The point of this demo: **context needs no special AG-UI class.** The frontend -sends a language choice (English / Arabic / German) over the AG-UI ``context`` -channel — a plain ``list[Context]`` of ``{description, value}`` items on -``RunAgentInput.context``. This example folds it into the system prompt with the -OpenAI Agents SDK's own native feature: ``Agent.instructions`` accepting a -callable ``(RunContextWrapper, Agent) -> str``, re-run every turn. - -Not registered in ``build_registry()``: ``server.py``'s wrapper -(``OpenAIAgentsAgent``) and ``translator_server.py``'s shared ``_stream()`` -both call ``Runner.run_streamed`` without ``context=``, so neither ever -forwards ``RunAgentInput.context`` to the SDK — ``to_sdk()`` hands it back on -``TranslatedInput.context``, but nothing in those two shared loops passes it -on. Rather than change either shared loop for one demo, ``stream()`` below -is this demo's own run loop — the same three calls those servers make -(``to_sdk`` / ``Runner.run_streamed`` / ``to_agui``), translator-driven, no -``OpenAIAgentsAgent`` wrapper and no FastAPI in this file, plus the one line -those loops skip: ``context=translated.context``, passed straight through as -the SDK's own ``context=`` and read back via ``RunContextWrapper.context`` in -``dynamic_instructions``. ``.dev/groq_server.py`` (gitignored, local Groq -dev) just calls ``stream()`` from its own route. No ``AGUIContext`` anywhere: -this demo has no state, only context, and context is just a list to read. +The frontend sends a language choice in ``RunAgentInput.context`` and the SDK +rebuilds its instructions for that request. """ from __future__ import annotations -from typing import AsyncIterator - -from agents import Agent, Runner, RunContextWrapper -from ag_ui.core import BaseEvent, Context, RunAgentInput - -from ag_ui_openai_agents import AGUITranslator +from agents import Agent, RunContextWrapper +from ag_ui.core import Context +from fastapi import FastAPI +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint from .constants import DEFAULT_MODEL BASE_INSTRUCTIONS = ( @@ -75,30 +54,6 @@ def create_dynamic_system_prompt_agent() -> Agent: ) -agent = create_dynamic_system_prompt_agent() - -# Reusable — to_sdk is stateless, to_agui spins up a fresh engine per call. -_translator = AGUITranslator() - - -async def stream(body: RunAgentInput) -> AsyncIterator[BaseEvent]: - """Run this demo for one AG-UI request, translator by hand, and yield AG-UI events. - - Any server can call this directly — it owns the HTTP/SSE plumbing, this - owns the run loop. The one line that makes the demo work: - ``context=translated.context``, forwarded to ``Runner.run_streamed`` so - ``dynamic_instructions`` can read it back. - """ - translated = _translator.to_sdk(body) - - run_agent = agent - if translated.tools: - run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools]) - - result = Runner.run_streamed( - run_agent, - input=translated.messages, - context=translated.context, - ) - async for event in _translator.to_agui(result, body): - yield event +agent = OpenAIAgentsAgent(create_dynamic_system_prompt_agent(), name="dynamic_system_prompt") +app = FastAPI(title="Dynamic system prompt AG-UI demo") +add_openai_agents_fastapi_endpoint(app, agent, "/") diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py index f1018e1fa0..00053b0295 100644 --- a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py @@ -17,7 +17,9 @@ from __future__ import annotations from agents import Agent, StopAtTools +from fastapi import FastAPI +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint from .constants import DEFAULT_MODEL # Must match the AG-UI client tool name the frontend declares in @@ -42,3 +44,8 @@ def create_human_in_the_loop_agent() -> Agent: instructions=INSTRUCTIONS, tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]), ) + + +agent = OpenAIAgentsAgent(create_human_in_the_loop_agent(), name="human_in_the_loop") +app = FastAPI(title="Human in the loop AG-UI demo") +add_openai_agents_fastapi_endpoint(app, agent, "/") diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py index e38ccb23dd..34d2dfa5cc 100644 --- a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py @@ -28,15 +28,20 @@ The frontend renders Approve/Reject; either choice comes back as the next ``RunAgentInput.forwarded_props["approval"]`` (``{"call_id", "approve"}``). -The run loop looks up the stored state, calls ``state.approve()`` / +The aggregate server looks up the stored state, calls ``state.approve()`` / ``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)`` instead of starting fresh from ``translated.messages``. """ from __future__ import annotations -from agents import Agent, function_tool +from agents import Agent, Runner, function_tool +from ag_ui.core import CustomEvent, EventType, RunAgentInput +from ag_ui.encoder import EventEncoder +from fastapi import FastAPI +from fastapi.responses import StreamingResponse +from ag_ui_openai_agents import AGUITranslator from .constants import DEFAULT_MODEL # Fake order book — good enough to make "approved" visibly do something. @@ -69,3 +74,73 @@ def create_human_in_the_loop_approval_agent() -> Agent: ), tools=[issue_refund], ) + + +agent = create_human_in_the_loop_approval_agent() +app = FastAPI(title="Human in the loop approval AG-UI demo") +_translator = AGUITranslator() +_encoder = EventEncoder() +_pending_approvals: dict[str, object] = {} + + +@app.post("/") +async def run(body: RunAgentInput) -> StreamingResponse: + """Run or resume the approval-gated agent.""" + + async def stream(): + decision = None + if isinstance(body.forwarded_props, dict): + decision = body.forwarded_props.get("approval") + + pending_state = _pending_approvals.pop(body.thread_id, None) + if decision and pending_state is not None: + item = next( + ( + item + for item in pending_state.get_interruptions() + if getattr(item.raw_item, "call_id", None) == decision.get("call_id") + ), + None, + ) + if item is not None: + if decision.get("approve"): + pending_state.approve(item) + else: + pending_state.reject(item) + result = Runner.run_streamed(agent, pending_state) + else: + translated = _translator.to_sdk(body) + run_agent = agent + if translated.tools: + run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools]) + result = Runner.run_streamed( + run_agent, input=translated.messages, context=translated.context + ) + + raw_events = [event async for event in result.stream_events()] + end_custom_event = None + if result.interruptions: + _pending_approvals[body.thread_id] = result.to_state() + end_custom_event = CustomEvent( + type=EventType.CUSTOM, + name="approval_request", + value=[ + { + "call_id": getattr(item.raw_item, "call_id", None), + "tool_name": item.tool_name, + "arguments": getattr(item.raw_item, "arguments", None), + } + for item in result.interruptions + ], + ) + + async def replay(): + for event in raw_events: + yield event + + async for event in _translator.to_agui( + replay(), body, end_custom_event=end_custom_event + ): + yield _encoder.encode(event) + + return StreamingResponse(stream(), media_type=_encoder.get_content_type()) diff --git a/integrations/openai-agents/python/examples/agents_examples/subagents.py b/integrations/openai-agents/python/examples/agents_examples/subagents.py index 53af3fb6cd..4f8f8dc741 100644 --- a/integrations/openai-agents/python/examples/agents_examples/subagents.py +++ b/integrations/openai-agents/python/examples/agents_examples/subagents.py @@ -17,7 +17,9 @@ from __future__ import annotations from agents import Agent +from fastapi import FastAPI +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint from .constants import DEFAULT_MODEL research_agent = Agent( @@ -74,3 +76,8 @@ def create_subagents_agent() -> Agent: ), ], ) + + +agent = OpenAIAgentsAgent(create_subagents_agent(), name="subagents") +app = FastAPI(title="Subagents AG-UI demo") +add_openai_agents_fastapi_endpoint(app, agent, "/") diff --git a/integrations/openai-agents/python/examples/agents_examples/tool_based_generative_ui.py b/integrations/openai-agents/python/examples/agents_examples/tool_based_generative_ui.py index 79ba38025a..0f5fff7f62 100644 --- a/integrations/openai-agents/python/examples/agents_examples/tool_based_generative_ui.py +++ b/integrations/openai-agents/python/examples/agents_examples/tool_based_generative_ui.py @@ -11,7 +11,9 @@ from __future__ import annotations from agents import Agent, StopAtTools +from fastapi import FastAPI +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint from .constants import DEFAULT_MODEL # Must match the AG-UI client tool name the frontend declares in @@ -36,3 +38,10 @@ def create_tool_based_generative_ui_agent() -> Agent: instructions=INSTRUCTIONS, tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]), ) + + +agent = OpenAIAgentsAgent( + create_tool_based_generative_ui_agent(), name="tool_based_generative_ui" +) +app = FastAPI(title="Tool-based generative UI AG-UI demo") +add_openai_agents_fastapi_endpoint(app, agent, "/") diff --git a/integrations/openai-agents/python/examples/server.py b/integrations/openai-agents/python/examples/server.py index 83b9c54655..1c6be7d996 100644 --- a/integrations/openai-agents/python/examples/server.py +++ b/integrations/openai-agents/python/examples/server.py @@ -1,225 +1,54 @@ -""" -Multi-agent example server — the wrapper style (an opinionated shortcut). - -Built with the package's serve layer: each demo is one OpenAIAgentsAgent + one -add_openai_agents_fastapi_endpoint call; the wrapper does to_sdk / -Runner.run_streamed / to_agui / SSE / lifecycle for you. Trades away the -control translator_server.py gives you (agent config fixed at construction, -server is FastAPI) for less code. - -Compare with translator_server.py (the translator by hand, recommended) to -see the trade: this file is shorter and has no run loop to get wrong; -translator_server.py shows every step and lets you branch mid-run. - - POST /agentic_chat ← plain conversation - POST /backend_tool_rendering ← server-executed @function_tool - POST /human_in_the_loop ← frontend-owned tool, StopAtTools - POST /tool_based_generative_ui ← frontend tool renders the content - POST /subagents ← multi-agent via agents-as-tools - POST /custom_lifecycle_events ← manual CUSTOM event bracketing the run - (routed by hand — see below) - POST /dynamic_system_prompt ← system prompt built from RunAgentInput.context - (routed by hand — see below) - POST /human_in_the_loop_approval ← backend tool gated by needs_approval, - resumed from result.interruptions - (routed by hand — see below) - - GET /health ← liveness check (lists all demos) - GET //health ← per-demo check +"""Aggregate FastAPI server for the OpenAI Agents SDK Dojo demos. -Run: - OPENAI_API_KEY=sk-... uv run python server.py - - # Auto-restart on code changes (this file, agents_examples/, or the - # package's own src/ — an editable install, so plain `uvicorn.run(app)` - # never notices those edits on its own): - RELOAD=1 OPENAI_API_KEY=sk-... uv run python server.py - -Test: - curl -N -X POST http://localhost:8022/agentic_chat \\ - -H 'Content-Type: application/json' \\ - -d '{ - "thread_id": "t1", - "run_id": "r1", - "messages": [{"id":"m1","role":"user","content":"Say hi in one sentence."}], - "tools": [], - "state": {}, - "context": [], - "forwarded_props": null - }' - -Expected event order for that request: - RUN_STARTED → STATE_SNAPSHOT → TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT (×N) - → TEXT_MESSAGE_END → MESSAGES_SNAPSHOT → RUN_FINISHED +Each demo owns a small FastAPI app. This server mounts those apps under the +Dojo feature paths and serves them together on port 8024. """ from __future__ import annotations -import logging import os from pathlib import Path import uvicorn -from agents import Runner -from ag_ui.core import CustomEvent, EventType, RunAgentInput -from ag_ui.encoder import EventEncoder from fastapi import FastAPI -from fastapi.responses import StreamingResponse -from ag_ui_openai_agents import ( - AGUITranslator, - OpenAIAgentsAgent, - add_openai_agents_fastapi_endpoint, +from agents_examples import ( + agentic_chat, + backend_tool_rendering, + custom_lifecycle_events, + dynamic_system_prompt, + human_in_the_loop, + human_in_the_loop_approval, + subagents, + tool_based_generative_ui, ) -from agents_examples import DemoConfig, build_registry -from agents_examples.dynamic_system_prompt import stream as dsp_stream - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -DEMOS: dict[str, DemoConfig] = build_registry() - -app = FastAPI(title="AG-UI × OpenAI Agents SDK examples (wrapper)") - -# The whole run loop: one wrapped agent + one endpoint per demo. Three demos -# need more than the wrapper can give them, so they're routed by hand below -# instead of through this loop: -# - custom_lifecycle_events: OpenAIAgentsAgent.run() calls -# to_agui(result, input) with no extra kwargs, so the wrapper can't -# forward DemoConfig.build_start_custom_event/build_end_custom_event — -# the CUSTOM events would silently never go out. -# - dynamic_system_prompt: OpenAIAgentsAgent.run() never passes context= -# to Runner.run_streamed, so the demo's whole point (reading -# RunAgentInput.context) would silently do nothing. -# - human_in_the_loop_approval: needs to inspect result.interruptions after the stream -# and, on the next request, resume from a stored RunState instead of -# translated.messages — the wrapper always starts fresh from the input. -_HAND_ROUTED = {"custom_lifecycle_events", "dynamic_system_prompt", "human_in_the_loop_approval"} - -for demo_name, demo in DEMOS.items(): - if demo_name in _HAND_ROUTED: - continue - add_openai_agents_fastapi_endpoint( - app, - OpenAIAgentsAgent(demo.agent, name=demo_name), - f"/{demo_name}", - ) - -_lifecycle_translator = AGUITranslator() -_lifecycle_encoder = EventEncoder() -_lifecycle_demo = DEMOS["custom_lifecycle_events"] - - -@app.post("/custom_lifecycle_events") -async def _run_custom_lifecycle_events(body: RunAgentInput) -> StreamingResponse: - async def _stream(): - translated = _lifecycle_translator.to_sdk(body) - agent = _lifecycle_demo.agent - if translated.tools: - agent = agent.clone(tools=[*agent.tools, *translated.tools]) - kwargs = {} - if _lifecycle_demo.build_start_custom_event is not None: - kwargs["start_custom_event"] = _lifecycle_demo.build_start_custom_event() - if _lifecycle_demo.build_end_custom_event is not None: - kwargs["end_custom_event"] = _lifecycle_demo.build_end_custom_event() - result = Runner.run_streamed(agent, input=translated.messages) - async for ag_event in _lifecycle_translator.to_agui(result, body, **kwargs): - yield _lifecycle_encoder.encode(ag_event) - - return StreamingResponse(_stream(), media_type=_lifecycle_encoder.get_content_type()) - - -_dsp_encoder = EventEncoder() - - -@app.post("/dynamic_system_prompt") -async def _run_dynamic_system_prompt(body: RunAgentInput) -> StreamingResponse: - async def _stream(): - async for ag_event in dsp_stream(body): - yield _dsp_encoder.encode(ag_event) - - return StreamingResponse(_stream(), media_type=_dsp_encoder.get_content_type()) - - -_approval_translator = AGUITranslator() -_approval_encoder = EventEncoder() -_approval_demo = DEMOS["human_in_the_loop_approval"] - -# thread_id -> paused RunState, waiting for an approve/reject decision on the -# next request. In-memory only — fine for a demo process, not a restart or a -# second server instance; a real app would use a session store instead. -_PENDING_APPROVALS: dict[str, object] = {} - - -@app.post("/human_in_the_loop_approval") -async def _run_approval(body: RunAgentInput) -> StreamingResponse: - async def _stream(): - agent = _approval_demo.agent - decision = None - forwarded = body.forwarded_props - if isinstance(forwarded, dict): - decision = forwarded.get("approval") - - pending_state = _PENDING_APPROVALS.pop(body.thread_id, None) - if decision and pending_state is not None: - item = next( - ( - i - for i in pending_state.get_interruptions() - if getattr(i.raw_item, "call_id", None) == decision.get("call_id") - ), - None, - ) - if item is not None: - if decision.get("approve"): - pending_state.approve(item) - else: - pending_state.reject(item) - result = Runner.run_streamed(agent, pending_state) - else: - translated = _approval_translator.to_sdk(body) - if translated.tools: - agent = agent.clone(tools=[*agent.tools, *translated.tools]) - result = Runner.run_streamed(agent, input=translated.messages) - - # result.interruptions is only known once the SDK's own stream is - # fully drained — there's no mid-stream event for it. to_agui() - # always puts RUN_FINISHED last, and the client drops anything that - # arrives after RUN_FINISHED (a finished run is done, full stop) — - # so the approval CustomEvent has to go out as end_custom_event - # (right before RUN_FINISHED), which means draining the raw SDK - # stream ourselves first instead of handing `result` straight to - # to_agui: end_custom_event has to already exist by the time we - # call it, and interruptions aren't known until the drain finishes. - raw_events = [event async for event in result.stream_events()] - - end_custom_event = None - if result.interruptions: - _PENDING_APPROVALS[body.thread_id] = result.to_state() - end_custom_event = CustomEvent( - type=EventType.CUSTOM, - name="approval_request", - value=[ - { - "call_id": getattr(item.raw_item, "call_id", None), - "tool_name": item.tool_name, - "arguments": getattr(item.raw_item, "arguments", None), - } - for item in result.interruptions - ], - ) - - async def _replay(): - for event in raw_events: - yield event - - async for ag_event in _approval_translator.to_agui( - _replay(), body, end_custom_event=end_custom_event - ): - yield _approval_encoder.encode(ag_event) - - return StreamingResponse(_stream(), media_type=_approval_encoder.get_content_type()) +DEMOS = { + "agentic_chat": agentic_chat.agent, + "backend_tool_rendering": backend_tool_rendering.agent, + "human_in_the_loop": human_in_the_loop.agent, + "human_in_the_loop_approval": human_in_the_loop_approval.agent, + "tool_based_generative_ui": tool_based_generative_ui.agent, + "subagents": subagents.agent, + "custom_lifecycle_events": custom_lifecycle_events.agent, + "dynamic_system_prompt": dynamic_system_prompt.agent, +} + +DEMO_APPS = { + "agentic_chat": agentic_chat.app, + "backend_tool_rendering": backend_tool_rendering.app, + "human_in_the_loop": human_in_the_loop.app, + "human_in_the_loop_approval": human_in_the_loop_approval.app, + "tool_based_generative_ui": tool_based_generative_ui.app, + "subagents": subagents.app, + "custom_lifecycle_events": custom_lifecycle_events.app, + "dynamic_system_prompt": dynamic_system_prompt.app, +} + +app = FastAPI(title="AG-UI × OpenAI Agents SDK examples") + +for demo_name, demo_app in DEMO_APPS.items(): + app.mount(f"/{demo_name}", demo_app, name=demo_name) @app.get("/health") @@ -231,9 +60,7 @@ def main() -> int: if not os.getenv("OPENAI_API_KEY"): print("Error: OPENAI_API_KEY required") return 1 - # 8022 is the port the AG-UI Dojo expects for this integration - # (apps/dojo/src/env.ts — OPENAI_AGENTS_PYTHON_URL). - port = int(os.getenv("PORT", "8022")) + port = int(os.getenv("PORT", "8024")) host = os.getenv("HOST", "0.0.0.0") print(f"Starting server on port {port} — agents: {list(DEMOS)}") if os.getenv("RELOAD"): diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index 97ba73a967..60dbbcbdd7 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -38,7 +38,7 @@ RELOAD=1 OPENAI_API_KEY=sk-... uv run python translator_server.py Test: - curl -N -X POST http://localhost:8022/agentic_chat \\ + curl -N -X POST http://localhost:8024/agentic_chat \\ -H 'Content-Type: application/json' \\ -d '{ "thread_id": "t1", @@ -253,9 +253,9 @@ def main() -> int: if not os.getenv("OPENAI_API_KEY"): print("Error: OPENAI_API_KEY required") return 1 - # 8022 is the port the AG-UI Dojo expects for this integration + # 8024 is the port the AG-UI Dojo expects for this integration # (apps/dojo/src/env.ts — OPENAI_AGENTS_PYTHON_URL). - port = int(os.getenv("PORT", "8022")) + port = int(os.getenv("PORT", "8024")) host = os.getenv("HOST", "0.0.0.0") print(f"Starting server on port {port} — agents: {list(DEMOS)}") if os.getenv("RELOAD"): diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index a6c9b1e3b3..f30f65f942 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -6,10 +6,10 @@ from __future__ import annotations -from typing import AsyncIterator +from typing import AsyncIterator, Callable from agents import Agent, RunConfig, Runner -from ag_ui.core import BaseEvent, RunAgentInput +from ag_ui.core import BaseEvent, CustomEvent, RunAgentInput from .translator import AGUITranslator @@ -43,6 +43,8 @@ def __init__( description: str = "", translator: AGUITranslator | None = None, run_config: RunConfig | None = None, + build_start_custom_event: Callable[[], CustomEvent] | None = None, + build_end_custom_event: Callable[[], CustomEvent] | None = None, ) -> None: """Wrap an SDK Agent. @@ -55,12 +57,18 @@ def __init__( run_config: Passed straight to Runner.run_streamed on every run — the place to set a non-OpenAI model provider (e.g. LiteLLM) or run-wide model settings. None uses the SDK defaults (native OpenAI). + build_start_custom_event: Lazily builds a CUSTOM event emitted after + RUN_STARTED for each run. + build_end_custom_event: Lazily builds a CUSTOM event emitted before + the terminal lifecycle event for each run. """ self.agent = agent self.name = name or agent.name self.description = description self._translator = translator or AGUITranslator() self._run_config = run_config + self._build_start_custom_event = build_start_custom_event + self._build_end_custom_event = build_end_custom_event async def run(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: """Run the agent for one AG-UI request and yield AG-UI events. @@ -88,7 +96,14 @@ async def run(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: agent, input=translated.messages, run_config=self._run_config, + context=translated.context, ) - async for event in self._translator.to_agui(result, input): + to_agui_kwargs = {} + if self._build_start_custom_event is not None: + to_agui_kwargs["start_custom_event"] = self._build_start_custom_event() + if self._build_end_custom_event is not None: + to_agui_kwargs["end_custom_event"] = self._build_end_custom_event() + + async for event in self._translator.to_agui(result, input, **to_agui_kwargs): yield event diff --git a/integrations/openai-agents/python/tests/test_agent.py b/integrations/openai-agents/python/tests/test_agent.py index a3588d5f56..43f5328f6b 100644 --- a/integrations/openai-agents/python/tests/test_agent.py +++ b/integrations/openai-agents/python/tests/test_agent.py @@ -8,7 +8,8 @@ RUN_FINISHED last) for one AG-UI request. - Client-declared tools are merged onto a per-request ``clone`` — the wrapped static agent is never mutated; without tools no clone happens. -- ``run_config`` passes through to ``Runner.run_streamed``. +- ``run_config`` and context pass through to ``Runner.run_streamed``. +- Optional lifecycle-event builders are forwarded to ``to_agui`` per run. - ``name`` defaults to the SDK agent's name. """ @@ -19,6 +20,8 @@ import pytest from ag_ui.core import ( + CustomEvent, + Context, EventType, RunAgentInput, Tool, @@ -62,7 +65,9 @@ def patched_runner(monkeypatch): calls: list[dict] = [] def fake_run_streamed(agent, *, input, run_config=None, **kwargs): - calls.append({"agent": agent, "input": input, "run_config": run_config}) + calls.append( + {"agent": agent, "input": input, "run_config": run_config, **kwargs} + ) result = MagicMock(spec=RunResultStreaming) result.stream_events.return_value = _empty_stream() return result @@ -113,6 +118,36 @@ def test_run_config_passes_through(patched_runner): assert patched_runner[0]["run_config"] is run_config +def test_context_passes_through(patched_runner): + run_input = _run_input() + run_input.context = [Context(description="Response language", value="German")] + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi")) + _collect(wrapper, run_input) + assert patched_runner[0]["context"] == run_input.context + + +def test_lifecycle_event_builders_pass_through(patched_runner, monkeypatch): + start = CustomEvent(type=EventType.CUSTOM, name="start", value={}) + end = CustomEvent(type=EventType.CUSTOM, name="end", value={}) + translated_calls: list[dict] = [] + wrapper = OpenAIAgentsAgent( + Agent(name="assistant", instructions="hi"), + build_start_custom_event=lambda: start, + build_end_custom_event=lambda: end, + ) + + original_to_agui = wrapper._translator.to_agui + + async def spy_to_agui(result, run_input, **kwargs): + translated_calls.append(kwargs) + async for event in original_to_agui(result, run_input, **kwargs): + yield event + + monkeypatch.setattr(wrapper._translator, "to_agui", spy_to_agui) + _collect(wrapper, _run_input()) + assert translated_calls == [{"start_custom_event": start, "end_custom_event": end}] + + def test_name_defaults_to_agent_name(): assert OpenAIAgentsAgent(Agent(name="helper", instructions="hi")).name == "helper" assert ( From 815bfc9e60214b86f1afe7cd199734873c9be1c8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 03:15:00 +0200 Subject: [PATCH 35/94] feat(openai-agents): add logging for agent run start events in endpoints --- .../openai-agents/python/examples/translator_server.py | 6 ++++++ .../python/src/ag_ui_openai_agents/endpoint.py | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index 60dbbcbdd7..8339bb231e 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -112,6 +112,12 @@ async def run(agent_name: str, body: RunAgentInput) -> StreamingResponse: demo = DEMOS.get(agent_name) if demo is None: raise HTTPException(status_code=404, detail=f"Unknown agent: {agent_name!r}") + logger.info( + "Starting agent run: agent=%s thread_id=%s run_id=%s", + agent_name, + body.thread_id, + body.run_id, + ) if agent_name == "human_in_the_loop_approval": return StreamingResponse( _stream_approval(demo, body), media_type=encoder.get_content_type() diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py index 5609739f76..b31a4395f1 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py @@ -6,6 +6,8 @@ from __future__ import annotations +import logging + from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse @@ -14,6 +16,8 @@ from .agent import OpenAIAgentsAgent +logger = logging.getLogger(__name__) + def add_openai_agents_fastapi_endpoint( app: FastAPI, @@ -32,6 +36,12 @@ def add_openai_agents_fastapi_endpoint( @app.post(path) async def openai_agents_endpoint(input_data: RunAgentInput, request: Request): """Run the agent and stream AG-UI events back to the client.""" + logger.info( + "Starting agent run: agent=%s thread_id=%s run_id=%s", + agent.name, + input_data.thread_id, + input_data.run_id, + ) accept_header = request.headers.get("accept") encoder = EventEncoder(accept=accept_header) From f559b6f822fbda92c6118301b59a4af2685a3990 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 03:28:10 +0200 Subject: [PATCH 36/94] feat(openai-agents): enhance wrappers to support all translator optional parameters --- apps/dojo/src/files.json | 2 +- .../examples/agents_examples/__init__.py | 10 +-- .../custom_lifecycle_events.py | 10 +-- .../python/examples/translator_server.py | 20 ++++-- .../python/src/ag_ui_openai_agents/agent.py | 65 ++++++++++++++----- .../openai-agents/python/tests/test_agent.py | 48 ++++++++++++-- 6 files changed, 117 insertions(+), 38 deletions(-) diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 32dc2927e6..80b28d2945 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4960,7 +4960,7 @@ }, { "name": "custom_lifecycle_events.py", - "content": "\"\"\"Custom lifecycle events — CUSTOM events bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. This ``DemoConfig`` sets\n``build_start_custom_event``/``build_end_custom_event``. The wrapper forwards\ntheir results to ``to_agui()``'s ``start_custom_event``/``end_custom_event``\nparams, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_event() -> CustomEvent:\n tokens = random.randint(20, 120)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"input_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)},\n )\n\n\ndef build_output_usage_event() -> CustomEvent:\n tokens = random.randint(120, 480)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"output_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)},\n )\n\n\nagent = OpenAIAgentsAgent(\n create_custom_lifecycle_events_agent(),\n name=\"custom_lifecycle_events\",\n build_start_custom_event=build_input_usage_event,\n build_end_custom_event=build_output_usage_event,\n)\napp = FastAPI(title=\"Custom lifecycle events AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "content": "\"\"\"Custom lifecycle events — CUSTOM events bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. The wrapper receives them as\n``start_custom_event``/``end_custom_event`` factories and forwards their\nresults to the same-named ``to_agui()`` parameters\nparams, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_event() -> CustomEvent:\n tokens = random.randint(20, 120)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"input_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)},\n )\n\n\ndef build_output_usage_event() -> CustomEvent:\n tokens = random.randint(120, 480)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"output_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)},\n )\n\n\nagent = OpenAIAgentsAgent(\n create_custom_lifecycle_events_agent(),\n name=\"custom_lifecycle_events\",\n start_custom_event=build_input_usage_event,\n end_custom_event=build_output_usage_event,\n)\napp = FastAPI(title=\"Custom lifecycle events AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py index f27a2ecae4..ea759eb3d7 100644 --- a/integrations/openai-agents/python/examples/agents_examples/__init__.py +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -4,7 +4,7 @@ assembles the full name → config map the aggregate servers serve, one route per key. Most demos only set ``agent``. custom_lifecycle_events also sets -``build_start_custom_event``/``build_end_custom_event``: optional callables +``start_custom_event``/``end_custom_event``: optional callables returning a ``CustomEvent``, which the shared loop forwards into ``to_agui(..., start_custom_event=..., end_custom_event=...)`` when present. No if-branch keyed by demo name, no separate router — one generic loop, @@ -42,8 +42,8 @@ class DemoConfig: """One demo route: the agent to run for it, plus optional lifecycle hooks.""" agent: Agent - build_start_custom_event: Callable[[], CustomEvent] | None = None - build_end_custom_event: Callable[[], CustomEvent] | None = None + start_custom_event: CustomEvent | Callable[[], CustomEvent] | None = None + end_custom_event: CustomEvent | Callable[[], CustomEvent] | None = None def build_registry() -> dict[str, DemoConfig]: @@ -61,8 +61,8 @@ def build_registry() -> dict[str, DemoConfig]: "subagents": DemoConfig(agent=create_subagents_agent()), "custom_lifecycle_events": DemoConfig( agent=create_custom_lifecycle_events_agent(), - build_start_custom_event=build_input_usage_event, - build_end_custom_event=build_output_usage_event, + start_custom_event=build_input_usage_event, + end_custom_event=build_output_usage_event, ), "dynamic_system_prompt": DemoConfig(agent=create_dynamic_system_prompt_agent()), } diff --git a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py index be1784f638..784604eeea 100644 --- a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py +++ b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py @@ -1,9 +1,9 @@ """Custom lifecycle events — CUSTOM events bracketing the run. Plain chat agent, same as agentic_chat — the point isn't the agent, it's -the two builder functions below. This ``DemoConfig`` sets -``build_start_custom_event``/``build_end_custom_event``. The wrapper forwards -their results to ``to_agui()``'s ``start_custom_event``/``end_custom_event`` +the two builder functions below. The wrapper receives them as +``start_custom_event``/``end_custom_event`` factories and forwards their +results to the same-named ``to_agui()`` parameters params, so one CUSTOM event goes out right after RUN_STARTED and another right before RUN_FINISHED. Only CustomEvent instances are accepted there; anything else @@ -56,8 +56,8 @@ def build_output_usage_event() -> CustomEvent: agent = OpenAIAgentsAgent( create_custom_lifecycle_events_agent(), name="custom_lifecycle_events", - build_start_custom_event=build_input_usage_event, - build_end_custom_event=build_output_usage_event, + start_custom_event=build_input_usage_event, + end_custom_event=build_output_usage_event, ) app = FastAPI(title="Custom lifecycle events AG-UI demo") add_openai_agents_fastapi_endpoint(app, agent, "/") diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index 8339bb231e..31f6684fac 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -17,8 +17,8 @@ POST /orchestrator ← multi-agent via agents-as-tools POST /custom_lifecycle_events ← manual CUSTOM event right after RUN_STARTED and right before RUN_FINISHED, via - DemoConfig.build_start_custom_event / - build_end_custom_event + DemoConfig.start_custom_event / + end_custom_event POST /human_in_the_loop_approval ← backend tool gated by needs_approval, resumed from result.interruptions (routed by hand — see below) @@ -232,10 +232,18 @@ async def _stream(demo: DemoConfig, body: RunAgentInput): # keeps this loop demo-agnostic: it just forwards whatever the registry # gave it. to_agui_kwargs = {} - if demo.build_start_custom_event: - to_agui_kwargs["start_custom_event"] = demo.build_start_custom_event() - if demo.build_end_custom_event: - to_agui_kwargs["end_custom_event"] = demo.build_end_custom_event() + if demo.start_custom_event: + to_agui_kwargs["start_custom_event"] = ( + demo.start_custom_event() + if callable(demo.start_custom_event) + else demo.start_custom_event + ) + if demo.end_custom_event: + to_agui_kwargs["end_custom_event"] = ( + demo.end_custom_event() + if callable(demo.end_custom_event) + else demo.end_custom_event + ) # 2 — Run the agent; to_agui() wraps the stream with the lifecycle events # (RUN_STARTED first, RUN_FINISHED / RUN_ERROR last), echoes the state diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index f30f65f942..58e349a05d 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import AsyncIterator, Callable +from typing import Any, AsyncIterator, Callable from agents import Agent, RunConfig, Runner from ag_ui.core import BaseEvent, CustomEvent, RunAgentInput @@ -43,8 +43,13 @@ def __init__( description: str = "", translator: AGUITranslator | None = None, run_config: RunConfig | None = None, - build_start_custom_event: Callable[[], CustomEvent] | None = None, - build_end_custom_event: Callable[[], CustomEvent] | None = None, + start_custom_event: CustomEvent | Callable[[], CustomEvent] | None = None, + initial_state: Any = None, + final_state: Any = None, + emit_messages_snapshot: bool = True, + end_custom_event: CustomEvent | Callable[[], CustomEvent] | None = None, + emit_run_error: bool = True, + run_error_message: str | None = None, ) -> None: """Wrap an SDK Agent. @@ -57,18 +62,32 @@ def __init__( run_config: Passed straight to Runner.run_streamed on every run — the place to set a non-OpenAI model provider (e.g. LiteLLM) or run-wide model settings. None uses the SDK defaults (native OpenAI). - build_start_custom_event: Lazily builds a CUSTOM event emitted after - RUN_STARTED for each run. - build_end_custom_event: Lazily builds a CUSTOM event emitted before - the terminal lifecycle event for each run. + start_custom_event: A CustomEvent, or a zero-argument factory that + builds one per run, emitted after RUN_STARTED. + initial_state: Passed to AGUITranslator.to_agui. Accepts the same + static, synchronous, or asynchronous state sources. + final_state: Passed to AGUITranslator.to_agui. Accepts the same + static, synchronous, or asynchronous state sources. + emit_messages_snapshot: Whether to emit MESSAGES_SNAPSHOT before + RUN_FINISHED. Defaults to True. + end_custom_event: A CustomEvent, or a zero-argument factory that + builds one per run, emitted before the terminal lifecycle event. + emit_run_error: Whether to emit RUN_ERROR when streaming fails. + Defaults to True. + run_error_message: Fixed RUN_ERROR message. None sends str(exc). """ self.agent = agent self.name = name or agent.name self.description = description self._translator = translator or AGUITranslator() self._run_config = run_config - self._build_start_custom_event = build_start_custom_event - self._build_end_custom_event = build_end_custom_event + self._start_custom_event = start_custom_event + self._initial_state = initial_state + self._final_state = final_state + self._emit_messages_snapshot = emit_messages_snapshot + self._end_custom_event = end_custom_event + self._emit_run_error = emit_run_error + self._run_error_message = run_error_message async def run(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: """Run the agent for one AG-UI request and yield AG-UI events. @@ -99,11 +118,25 @@ async def run(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: context=translated.context, ) - to_agui_kwargs = {} - if self._build_start_custom_event is not None: - to_agui_kwargs["start_custom_event"] = self._build_start_custom_event() - if self._build_end_custom_event is not None: - to_agui_kwargs["end_custom_event"] = self._build_end_custom_event() - - async for event in self._translator.to_agui(result, input, **to_agui_kwargs): + start_custom_event = self._resolve_custom_event(self._start_custom_event) + end_custom_event = self._resolve_custom_event(self._end_custom_event) + + async for event in self._translator.to_agui( + result, + input, + start_custom_event=start_custom_event, + initial_state=self._initial_state, + final_state=self._final_state, + emit_messages_snapshot=self._emit_messages_snapshot, + end_custom_event=end_custom_event, + emit_run_error=self._emit_run_error, + run_error_message=self._run_error_message, + ): yield event + + @staticmethod + def _resolve_custom_event( + source: CustomEvent | Callable[[], CustomEvent] | None, + ) -> CustomEvent | None: + """Resolve a fixed custom event or a per-run event factory.""" + return source() if callable(source) else source diff --git a/integrations/openai-agents/python/tests/test_agent.py b/integrations/openai-agents/python/tests/test_agent.py index 43f5328f6b..71e36c71d9 100644 --- a/integrations/openai-agents/python/tests/test_agent.py +++ b/integrations/openai-agents/python/tests/test_agent.py @@ -9,7 +9,7 @@ - Client-declared tools are merged onto a per-request ``clone`` — the wrapped static agent is never mutated; without tools no clone happens. - ``run_config`` and context pass through to ``Runner.run_streamed``. -- Optional lifecycle-event builders are forwarded to ``to_agui`` per run. +- Every ``to_agui`` option is forwarded with the same public name. - ``name`` defaults to the SDK agent's name. """ @@ -126,14 +126,21 @@ def test_context_passes_through(patched_runner): assert patched_runner[0]["context"] == run_input.context -def test_lifecycle_event_builders_pass_through(patched_runner, monkeypatch): +def test_to_agui_options_pass_through(patched_runner, monkeypatch): start = CustomEvent(type=EventType.CUSTOM, name="start", value={}) end = CustomEvent(type=EventType.CUSTOM, name="end", value={}) + initial_state = lambda: {"phase": "initial"} + final_state = lambda: {"phase": "final"} translated_calls: list[dict] = [] wrapper = OpenAIAgentsAgent( Agent(name="assistant", instructions="hi"), - build_start_custom_event=lambda: start, - build_end_custom_event=lambda: end, + start_custom_event=lambda: start, + initial_state=initial_state, + final_state=final_state, + emit_messages_snapshot=False, + end_custom_event=lambda: end, + emit_run_error=False, + run_error_message="safe error", ) original_to_agui = wrapper._translator.to_agui @@ -145,7 +152,38 @@ async def spy_to_agui(result, run_input, **kwargs): monkeypatch.setattr(wrapper._translator, "to_agui", spy_to_agui) _collect(wrapper, _run_input()) - assert translated_calls == [{"start_custom_event": start, "end_custom_event": end}] + assert translated_calls == [ + { + "start_custom_event": start, + "initial_state": initial_state, + "final_state": final_state, + "emit_messages_snapshot": False, + "end_custom_event": end, + "emit_run_error": False, + "run_error_message": "safe error", + } + ] + + +def test_custom_event_factories_run_once_per_request(patched_runner): + calls = {"start": 0, "end": 0} + + def start_custom_event(): + calls["start"] += 1 + return CustomEvent(type=EventType.CUSTOM, name="start", value={}) + + def end_custom_event(): + calls["end"] += 1 + return CustomEvent(type=EventType.CUSTOM, name="end", value={}) + + wrapper = OpenAIAgentsAgent( + Agent(name="assistant", instructions="hi"), + start_custom_event=start_custom_event, + end_custom_event=end_custom_event, + ) + _collect(wrapper, _run_input()) + _collect(wrapper, _run_input()) + assert calls == {"start": 2, "end": 2} def test_name_defaults_to_agent_name(): From 3426b18a4efc1b5f090f5467976cedb5c723c74f Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 21:31:57 +0200 Subject: [PATCH 37/94] feat(examples): add AG-UI Docs Copilot with documentation specialist agent --- apps/dojo/scripts/generate-content-json.ts | 5 +- apps/dojo/src/agents.ts | 1 + .../(v2)/ag_ui_docs_copilot/README.mdx | 37 +++++++ .../feature/(v2)/ag_ui_docs_copilot/page.tsx | 63 +++++++++++ apps/dojo/src/app/globals.css | 4 +- apps/dojo/src/config.ts | 6 ++ apps/dojo/src/files.json | 20 ++++ apps/dojo/src/menu.ts | 1 + apps/dojo/src/types/integration.ts | 1 + .../openai-agents/python/examples/README.md | 80 ++++++++------ .../agents_examples/ag_ui_docs_copilot.py | 102 ++++++++++++++++++ .../openai-agents/python/examples/server.py | 6 ++ .../tests/test_ag_ui_docs_copilot_example.py | 33 ++++++ 13 files changed, 325 insertions(+), 34 deletions(-) create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx create mode 100644 apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx create mode 100644 integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py create mode 100644 integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py diff --git a/apps/dojo/scripts/generate-content-json.ts b/apps/dojo/scripts/generate-content-json.ts index a8399ed5ce..e0aa0055d8 100644 --- a/apps/dojo/scripts/generate-content-json.ts +++ b/apps/dojo/scripts/generate-content-json.ts @@ -91,7 +91,10 @@ async function getFeatureFrontendFiles(featureId: string) { const retrievedFiles = []; for (const fileName of featureFiles) { - retrievedFiles.push(await getFile(featurePath, fileName)); + const filePath = path.join(featurePath, fileName); + if (fs.existsSync(filePath)) { + retrievedFiles.push(await getFile(featurePath, fileName)); + } } return retrievedFiles; diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 6953eead34..8dbebc6ac9 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -637,6 +637,7 @@ export const agentsIntegrations = { (path) => new HttpAgent({ url: `${envVars.openaiAgentsPythonUrl}/${path}/` }), { + ag_ui_docs_copilot: "ag_ui_docs_copilot", agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx new file mode 100644 index 0000000000..8a0402fbd9 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx @@ -0,0 +1,37 @@ +# AG-UI Docs Copilot + +This example is a small documentation assistant for the AG-UI integration with +the OpenAI Agents SDK. + +The main **Copilot** handles normal conversation without loading the +documentation. For AG-UI documentation or code questions, it calls a second +OpenAI Agents SDK agent, **AG-UI Documentation Specialist**, through +`Agent.as_tool()`. Only the specialist receives the local `README.md`. + +## What it demonstrates + +```text +Copilot → Documentation Specialist (local README) + ↓ +RunAgentInput → to_sdk() → Runner.run_streamed() → to_agui() → SSE +``` + +- Local documentation with no internet request or retrieval framework +- An OpenAI Agents SDK documentation specialist used as a tool by Copilot +- A direct, visible AG-UI translator endpoint +- Streaming lifecycle, text, and tool-call events to CopilotKit + +## Why the documentation is loaded directly + +The integration README is small enough for this focused demo, so it is read +once and included in the agents' instructions. This keeps the example +deterministic and easy to copy. A production assistant with many or large +documents should replace this with its own retrieval system. + +## Try it + +- Ask how `AGUITranslator.to_sdk()` and `to_agui()` connect the frontend and + SDK. +- Ask the Documentation Specialist to generate a minimal FastAPI streaming endpoint. +- Ask which translator options control lifecycle events, snapshots, state, and + errors. diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx new file mode 100644 index 0000000000..7f52af9864 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React from "react"; +import { CopilotKit } from "@copilotkit/react-core"; +import { + CopilotChat, + useConfigureSuggestions, +} from "@copilotkit/react-core/v2"; +import "@copilotkit/react-core/v2/styles.css"; + +interface AGUIDocsCopilotProps { + params: Promise<{ integrationId: string }>; +} + +const AGUIDocsCopilot: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + ); +}; + +const DocsChat = () => { + useConfigureSuggestions({ + suggestions: [ + { + title: "Stream an existing agent", + message: + "Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_sdk(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.", + }, + { + title: "Map SDK events to AG-UI", + message: + "Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.", + }, + { + title: "Choose an integration layer", + message: + "Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.", + }, + ], + available: "always", + }); + + return ( +
+
+ +
+
+ ); +}; + +export default AGUIDocsCopilot; diff --git a/apps/dojo/src/app/globals.css b/apps/dojo/src/app/globals.css index 28208d8591..490a90592f 100644 --- a/apps/dojo/src/app/globals.css +++ b/apps/dojo/src/app/globals.css @@ -309,7 +309,7 @@ } @utility mix-from-* { - --tw-mix-from-color: --value(--color-*, color, [color]); + --tw-mix-from-color: --value(--color-*, [color]); --tw-mix-from-opacity: --modifier(--opacity-*, [percentage]); --tw-mix-from-opacity: calc(--modifier(number) * 1%); --tw-mix-from-opacity: calc(--modifier([number]) * 100%); @@ -326,7 +326,7 @@ } @utility mix-to-* { - --tw-mix-to-color: --value(--color-*, color, [color]); + --tw-mix-to-color: --value(--color-*, [color]); --tw-mix-to-opacity: --modifier(--opacity-*, [percentage]); --tw-mix-to-opacity: calc(--modifier(number) * 1%); --tw-mix-to-opacity: calc(--modifier([number]) * 100%); diff --git a/apps/dojo/src/config.ts b/apps/dojo/src/config.ts index dafc9616e6..3319a72cbd 100644 --- a/apps/dojo/src/config.ts +++ b/apps/dojo/src/config.ts @@ -17,6 +17,12 @@ function createFeatureConfig({ } export const featureConfig: FeatureConfig[] = [ + createFeatureConfig({ + id: "ag_ui_docs_copilot", + name: "AG-UI Docs Copilot", + description: "Ask questions about the OpenAI Agents integration and delegate code snippets to a specialist agent", + tags: ["AG-UI", "OpenAI Agents", "Documentation", "Copilot"], + }), createFeatureConfig({ id: "agentic_chat", name: "Agentic Chat", diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 80b28d2945..2dac76bd7a 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4813,6 +4813,26 @@ "type": "file" } ], + "openai-agents-python::ag_ui_docs_copilot": [ + { + "name": "page.tsx", + "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_sdk(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default AGUIDocsCopilot;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nThe main **Copilot** handles normal conversation without loading the\ndocumentation. For AG-UI documentation or code questions, it calls a second\nOpenAI Agents SDK agent, **AG-UI Documentation Specialist**, through\n`Agent.as_tool()`. Only the specialist receives the local `README.md`.\n\n## What it demonstrates\n\n```text\nCopilot → Documentation Specialist (local README)\n ↓\nRunAgentInput → to_sdk() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- An OpenAI Agents SDK documentation specialist used as a tool by Copilot\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded directly\n\nThe integration README is small enough for this focused demo, so it is read\nonce and included in the agents' instructions. This keeps the example\ndeterministic and easy to copy. A production assistant with many or large\ndocuments should replace this with its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_sdk()` and `to_agui()` connect the frontend and\n SDK.\n- Ask the Documentation Specialist to generate a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", + "language": "markdown", + "type": "file" + }, + { + "name": "ag_ui_docs_copilot.py", + "content": "\"\"\"AG-UI documentation assistant with a code-writing agent as a tool.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom agents import Agent, Runner\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui_openai_agents import AGUITranslator\n\nfrom .constants import DEFAULT_MODEL\n\n\nDOCS = Path(__file__).resolve().parents[2].joinpath(\"README.md\").read_text(encoding=\"utf-8\")\n\n# Documentation specialist: knows ag-ui-openai-agents docs\ncode_agent_instructions = f\"\"\"You are the technical specialist for the AG-UI\nintegration with the OpenAI Agents SDK. The documentation below is your source\nof truth. Help developers understand and implement the integration: the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\n\n{DOCS}\n\n\"\"\"\n\ndocs_agent = Agent(\n name=\"AG-UI Documentation Specialist\",\n model=DEFAULT_MODEL,\n instructions=code_agent_instructions\n)\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call ask_ag_ui_docs before\nanswering. Treat the specialist as the source of truth for integration details.\nUse only the part of its result that answers the user's question. Do not invent\nintegration-specific behavior or claim details the specialist did not provide.\nFor code requests, make sure the specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n docs_agent.as_tool(\n tool_name=\"ask_ag_ui_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n )\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_sdk(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", + "language": "python", + "type": "file" + } + ], "openai-agents-python::agentic_chat": [ { "name": "page.tsx", diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index 592fce9ffb..78375f23bc 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -381,6 +381,7 @@ export const menuIntegrations = [ id: "openai-agents-python", name: "OpenAI Agents SDK (Python)", features: [ + "ag_ui_docs_copilot", "agentic_chat", "backend_tool_rendering", "human_in_the_loop", diff --git a/apps/dojo/src/types/integration.ts b/apps/dojo/src/types/integration.ts index 91296b5944..2c29e97561 100644 --- a/apps/dojo/src/types/integration.ts +++ b/apps/dojo/src/types/integration.ts @@ -1,6 +1,7 @@ import type { menuIntegrations } from "../menu"; export type Feature = + | "ag_ui_docs_copilot" | "agentic_chat" | "agentic_generative_ui" | "human_in_the_loop" diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index e51bd98e35..b801f27116 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -1,16 +1,16 @@ # OpenAI Agents SDK examples -Runnable demos for `ag_ui_openai_agents`, one FastAPI route per agent. Two -servers, same demos and same output, showing the two ways to build: - -- **`translator_server.py`** — the translator by hand (`to_sdk` → - `Runner.run_streamed` → `to_agui`). **Recommended.** Full control of the - agent and the server; `AGUITranslator` is just an events translator. Every - step is visible; branch mid-run if you need to. -- **`server.py`** — the serve layer (`OpenAIAgentsAgent` + - `add_openai_agents_fastapi_endpoint`). One wrapped agent + one endpoint call - per demo, no run loop to get wrong — an opinionated shortcut that trades - control for less code. +Runnable demos for `ag_ui_openai_agents`, one mounted FastAPI app per agent. +The aggregate server shows both integration styles: + +- **`ag_ui_docs_copilot`** handles normal conversation with a small main + Copilot and delegates AG-UI documentation and code questions to an + `AG-UI Documentation Specialist` agent as a tool. +- The remaining focused feature apps use **`OpenAIAgentsAgent`** and + `add_openai_agents_fastapi_endpoint` where their run does not require custom + control. +- **`translator_server.py`** remains a compact, centralized direct-translator + reference for the original focused demos. Model provider is **native OpenAI** (`OPENAI_API_KEY`) — the translators are provider-agnostic, but these @@ -34,7 +34,7 @@ override with `PORT`). ## Testing ```bash -curl -N -X POST http://localhost:8024/agentic_chat \ +curl -N -X POST http://localhost:8024/agentic_chat/ \ -H 'Content-Type: application/json' \ -d '{ "thread_id": "t1", @@ -47,9 +47,8 @@ curl -N -X POST http://localhost:8024/agentic_chat \ }' ``` -Swap the path to hit a different demo — `GET /health` lists every registered -agent. Demos map 1:1 onto the AG-UI Dojo feature pages, plus a multi-agent -pattern (`orchestrator`). +Swap the path to hit a different demo. `GET /health` on the aggregate server +lists every registered agent. Demos map 1:1 onto the AG-UI Dojo feature pages. > The stateful demos (`shared_state`, `agentic_generative_ui`, > `predictive_state_updates`) are shelved together with the `AGUIContext` @@ -57,20 +56,36 @@ pattern (`orchestrator`). ## Agents +### `ag_ui_docs_copilot` + +The main Copilot handles normal conversation without carrying the documentation +in its instructions. Documentation and code questions are delegated to an +`AG-UI Documentation Specialist`, which receives the integration's local +`README.md` through the SDK's `Agent.as_tool()` API. Its endpoint keeps the +direct `to_sdk` → `Runner.run_streamed` → `to_agui` flow visible and adds no +retrieval framework, vector database, or network dependency. + +**Try:** `"Explain how to connect my existing OpenAI Agents SDK agent to AG-UI, +then ask the Documentation Specialist for the smallest FastAPI streaming +endpoint."` + ### `agentic_chat` + Plain conversation, no tools. Exercises `TEXT_MESSAGE_START/CONTENT/END` only — the smallest possible smoke test. **Try:** `"Say hi in one sentence."` ### `backend_tool_rendering` + A server-side `@function_tool` (`get_weather`) the SDK executes itself. Exercises `TOOL_CALL_START/ARGS/END` + `TOOL_CALL_RESULT`. **Try:** `"What's the weather in Berlin?"` ### `human_in_the_loop` -A *frontend*-owned tool (`generate_task_steps`) declared by the client in + +A _frontend_-owned tool (`generate_task_steps`) declared by the client in `RunAgentInput.tools`, not the server. Uses the SDK's built-in `tool_use_behavior=StopAtTools(...)` so the run ends the moment the model calls it — the tool body never executes server-side. The frontend renders @@ -81,15 +96,16 @@ the steps, gets user approval, and sends the result back as an ordinary a `generate_task_steps` tool definition in `RunAgentInput.tools` (e.g. the AG-UI Dojo's human-in-the-loop page, which uses the same tool name). -> **vs. `human_in_the_loop_approval` (below):** here the tool has *no server -> implementation* — only the browser can run it, so there's nothing to gate. +> **vs. `human_in_the_loop_approval` (below):** here the tool has _no server +> implementation_ — only the browser can run it, so there's nothing to gate. > `human_in_the_loop_approval` is the opposite case: a real backend tool, -> paused by the SDK's own approval API before *its* body runs. Different +> paused by the SDK's own approval API before _its_ body runs. Different > problem, different mechanism — see the comparison table under > `human_in_the_loop_approval`. ### `tool_based_generative_ui` -Another frontend-owned tool (`generate_haiku`), but here the tool call *is* + +Another frontend-owned tool (`generate_haiku`), but here the tool call _is_ the deliverable: the frontend renders the haiku card straight from the streamed `TOOL_CALL_ARGS` — no approval round-trip. Same `StopAtTools` mechanics as `human_in_the_loop`. @@ -98,7 +114,8 @@ mechanics as `human_in_the_loop`. `generate_haiku` tool definition (Dojo's tool-based generative UI page). ### `human_in_the_loop_approval` -A *backend*-owned tool (`issue_refund`) that requires approval before it + +A _backend_-owned tool (`issue_refund`) that requires approval before it runs — the SDK's `needs_approval=True` (`agents.tool.function_tool`), not an AG-UI concept. Unlike `human_in_the_loop`, the tool has a real server-side implementation; the SDK itself pauses the run and reports a @@ -113,7 +130,7 @@ through the shared loop: a `CustomEvent(name="approval_request")` as `to_agui()`'s `end_custom_event` — right before `RUN_FINISHED`, not after (the client drops anything that arrives once a run is marked finished). -2. The client's decision comes back on the *next* request as +2. The client's decision comes back on the _next_ request as `RunAgentInput.forwarded_props["approval"]` (`{"call_id", "approve"}`). The server looks up the stored state, calls `state.approve()` / `state.reject()`, and resumes with `Runner.run_streamed(agent, state)` @@ -126,17 +143,18 @@ page does this). **`human_in_the_loop_approval` vs. `human_in_the_loop` — same goal, different mechanism:** -| | `human_in_the_loop` | `human_in_the_loop_approval` | -|---|---|---| -| Who owns the tool | Frontend — no server implementation exists | Backend — real `@function_tool` code | -| What pauses the run | `StopAtTools`, the instant the model calls it | The SDK's own `needs_approval` gate, before the body runs | -| How the pause surfaces | Normal `TOOL_CALL_*` events, mid-stream | `result.interruptions`, only known after the stream ends | -| How the decision comes back | An ordinary `ToolMessage` in the next request's `messages` | `RunAgentInput.forwarded_props["approval"]`, resumed via a stored `RunState` | -| Why | The action can *only* happen client-side (render UI, wait on a click) | The backend *can* do it, but shouldn't without sign-off first | +| | `human_in_the_loop` | `human_in_the_loop_approval` | +| --------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| Who owns the tool | Frontend — no server implementation exists | Backend — real `@function_tool` code | +| What pauses the run | `StopAtTools`, the instant the model calls it | The SDK's own `needs_approval` gate, before the body runs | +| How the pause surfaces | Normal `TOOL_CALL_*` events, mid-stream | `result.interruptions`, only known after the stream ends | +| How the decision comes back | An ordinary `ToolMessage` in the next request's `messages` | `RunAgentInput.forwarded_props["approval"]`, resumed via a stored `RunState` | +| Why | The action can _only_ happen client-side (render UI, wait on a click) | The backend _can_ do it, but shouldn't without sign-off first | + +### `subagents` -### `orchestrator` Multi-agent via the SDK's **agents-as-tools** pattern (`Agent.as_tool()`) — -control never transfers, the orchestrator calls `research_agent`, +control never transfers, the supervisor calls `research_agent`, `writer_agent`, and `critic_agent` as tools and synthesizes the final answer itself. Each specialist invocation appears to the client as a normal `TOOL_CALL_*` + `TOOL_CALL_RESULT` sequence; the nested agents' inner turns diff --git a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py new file mode 100644 index 0000000000..936302ac2b --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py @@ -0,0 +1,102 @@ +"""AG-UI documentation assistant with a code-writing agent as a tool.""" + +from __future__ import annotations + +from pathlib import Path + +from ag_ui.core import RunAgentInput +from ag_ui.encoder import EventEncoder +from agents import Agent, Runner +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse + +from ag_ui_openai_agents import AGUITranslator + +from .constants import DEFAULT_MODEL + + +DOCS = Path(__file__).resolve().parents[2].joinpath("README.md").read_text(encoding="utf-8") + +# Documentation specialist: knows ag-ui-openai-agents docs +code_agent_instructions = f"""You are the technical specialist for the AG-UI +integration with the OpenAI Agents SDK. The documentation below is your source +of truth. Help developers understand and implement the integration: the +AGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE +endpoints, tools, client tools, context, state, lifecycle events, errors, and +testing. + +Answer only the user's question. Retrieve and explain only the relevant parts +of the documentation; do not add a broad tutorial, related features, or extra +options unless the user asks. Be concise and practical. For code requests, +provide only the smallest complete, production-readable Python snippet needed +for the request, followed by a short explanation. Use documented APIs only. Do +not invent behavior or configuration. If the documentation does not establish +an answer, say so briefly instead of guessing. + + +{DOCS} + +""" + +docs_agent = Agent( + name="AG-UI Documentation Specialist", + model=DEFAULT_MODEL, + instructions=code_agent_instructions +) + +# Main Copilot: handles normal conversation and delegates documentation work. +copilot_instructions = """You are the developer-facing Copilot for an AG-UI +application. Answer only what the user asks. Be clear, practical, and concise; +do not add a tutorial, unrelated details, alternatives, or follow-up work +unless requested. Handle ordinary conversation directly. + +For any question or request about AG-UI, the OpenAI Agents SDK, translator +APIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle +events, errors, tests, or Python implementation, call ask_ag_ui_docs before +answering. Treat the specialist as the source of truth for integration details. +Use only the part of its result that answers the user's question. Do not invent +integration-specific behavior or claim details the specialist did not provide. +For code requests, make sure the specialist is called.""" + +copilot_agent = Agent( + name="AG-UI Docs Copilot", + model=DEFAULT_MODEL, + instructions=copilot_instructions, + tools=[ + docs_agent.as_tool( + tool_name="ask_ag_ui_docs", + tool_description=( + "Provide authoritative AG-UI and OpenAI Agents SDK guidance, " + "including documented Python integration snippets." + ), + ) + ], +) + +# AGUI Translator Integration +app = FastAPI(title="AG-UI Docs Copilot") +translator = AGUITranslator() + +@app.post("/") +async def run_ag_ui_docs_copilot( + body: RunAgentInput, request: Request +) -> StreamingResponse: + """Translate one AG-UI request into an SDK run and stream it back.""" + encoder = EventEncoder(accept=request.headers.get("accept")) + + async def stream(): + # AGUI input -> OpenAI SDK + translated_input = translator.to_sdk(body) + + # normal OpenAI SDK streaming run + result = Runner.run_streamed( + copilot_agent, + input=translated_input.messages, + context=translated_input.context, + ) + + # OpenAI SDK -> AGUI events + async for event in translator.to_agui(result, body): + yield encoder.encode(event) + + return StreamingResponse(stream(), media_type=encoder.get_content_type()) diff --git a/integrations/openai-agents/python/examples/server.py b/integrations/openai-agents/python/examples/server.py index 1c6be7d996..84e458f21e 100644 --- a/integrations/openai-agents/python/examples/server.py +++ b/integrations/openai-agents/python/examples/server.py @@ -10,9 +10,11 @@ from pathlib import Path import uvicorn +from agents import set_tracing_disabled from fastapi import FastAPI from agents_examples import ( + ag_ui_docs_copilot, agentic_chat, backend_tool_rendering, custom_lifecycle_events, @@ -23,7 +25,10 @@ tool_based_generative_ui, ) +set_tracing_disabled(True) + DEMOS = { + "ag_ui_docs_copilot": ag_ui_docs_copilot.copilot_agent, "agentic_chat": agentic_chat.agent, "backend_tool_rendering": backend_tool_rendering.agent, "human_in_the_loop": human_in_the_loop.agent, @@ -35,6 +40,7 @@ } DEMO_APPS = { + "ag_ui_docs_copilot": ag_ui_docs_copilot.app, "agentic_chat": agentic_chat.app, "backend_tool_rendering": backend_tool_rendering.app, "human_in_the_loop": human_in_the_loop.app, diff --git a/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py b/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py new file mode 100644 index 0000000000..c1c5fc29f8 --- /dev/null +++ b/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py @@ -0,0 +1,33 @@ +"""Tests for the AG-UI Docs Copilot example.""" + +from __future__ import annotations + +import inspect +import sys +from pathlib import Path + +EXAMPLES = Path(__file__).resolve().parents[1] / "examples" +sys.path.insert(0, str(EXAMPLES)) + +from agents_examples import ag_ui_docs_copilot # noqa: E402 + + +def test_docs_copilot_loads_the_integration_readme() -> None: + assert "AG-UI × OpenAI Agents SDK" in ag_ui_docs_copilot.DOCS + assert "AGUITranslator" in ag_ui_docs_copilot.DOCS + + +def test_docs_copilot_has_a_documentation_specialist_tool() -> None: + assert ag_ui_docs_copilot.copilot_agent.name == "AG-UI Docs Copilot" + assert {tool.name for tool in ag_ui_docs_copilot.copilot_agent.tools} == { + "ask_ag_ui_docs" + } + assert ag_ui_docs_copilot.docs_agent.name == "AG-UI Documentation Specialist" + + +def test_docs_copilot_keeps_the_direct_translator_flow_visible() -> None: + assert "/" in {route.path for route in ag_ui_docs_copilot.app.routes} + source = inspect.getsource(ag_ui_docs_copilot.run_ag_ui_docs_copilot) + assert "translator.to_sdk(body)" in source + assert "Runner.run_streamed(" in source + assert "translator.to_agui(result, body)" in source From b06b1d9dca09deeee61be892eb7360cf6ec65efe Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 21:54:23 +0200 Subject: [PATCH 38/94] feat(openai-agents): update package name to ag-ui-openai-agents and adjust Python version requirement --- integrations/openai-agents/python/README.md | 2 +- .../openai-agents/python/pyproject.toml | 7 +- integrations/openai-agents/python/uv.lock | 1504 ++++++++--------- 3 files changed, 722 insertions(+), 791 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 7a152e7f91..a472a73917 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -35,7 +35,7 @@ is a `MESSAGES_SNAPSHOT` by default — see ## Install ```bash -pip install ag-ui-openai-agent-sdk +pip install ag-ui-openai-agents ``` For local development this package uses [uv](https://docs.astral.sh/uv/): diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml index d251f21ab1..7655dd77c6 100644 --- a/integrations/openai-agents/python/pyproject.toml +++ b/integrations/openai-agents/python/pyproject.toml @@ -1,17 +1,18 @@ [project] -name = "ag-ui-openai-agent-sdk" +name = "ag-ui-openai-agents" version = "0.1.0" description = "AG-UI integration for OpenAI Agent SDK" readme = "README.md" authors = [ - { name = "Abdelrahman Abozied" }, + { name = "Abdelrahman Abozied", email = "me@abdelrahman.ai" }, ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "ag-ui-protocol>=0.1.18", "openai-agents>=0.8.4", "pydantic>=2.13.4", "fastapi>=0.115.12", + "uvicorn>=0.51.0", ] [dependency-groups] diff --git a/integrations/openai-agents/python/uv.lock b/integrations/openai-agents/python/uv.lock index bdb5d8b17b..158b9526d7 100644 --- a/integrations/openai-agents/python/uv.lock +++ b/integrations/openai-agents/python/uv.lock @@ -1,28 +1,30 @@ version = 1 revision = 3 -requires-python = ">=3.9" +requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version < '3.10'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", ] [[package]] -name = "ag-ui-openai-agent-sdk" +name = "ag-ui-openai-agents" version = "0.1.0" source = { editable = "." } dependencies = [ { name = "ag-ui-protocol" }, - { name = "fastapi", version = "0.128.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "fastapi", version = "0.139.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "openai-agents", version = "0.8.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "openai-agents", version = "0.17.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fastapi" }, + { name = "openai-agents" }, { name = "pydantic" }, + { name = "uvicorn" }, ] [package.dev-dependencies] dev = [ - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, ] [package.metadata] @@ -31,6 +33,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.115.12" }, { name = "openai-agents", specifier = ">=0.8.4" }, { name = "pydantic", specifier = ">=2.13.4" }, + { name = "uvicorn", specifier = ">=0.51.0" }, ] [package.metadata.requires-dev] @@ -38,14 +41,14 @@ dev = [{ name = "pytest", specifier = ">=8.0" }] [[package]] name = "ag-ui-protocol" -version = "0.1.18" +version = "0.1.19" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/d7/5711eada86da9bd7684e58645653a1693ef20b66cc3efbb1deeafef80f8d/ag_ui_protocol-0.1.18.tar.gz", hash = "sha256:b37c672c3fd6bac12b316c39f45ad9db9f137bbb885489c79f268507029a22ff", size = 9937, upload-time = "2026-04-21T20:44:59.151Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/10/4ad299267a7d04b89935aa99eef62979758fcf95aee9f8bb5d70c35b1be1/ag_ui_protocol-0.1.19.tar.gz", hash = "sha256:43c27f60d41712dcad0e9e0a203cbdf1c8e248b22417374c5c68321c448af4ea", size = 10720, upload-time = "2026-06-02T17:26:15.627Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/74/913c9b8fc566c6da650aecbddf25a5d8186b54138df265eb9eb546f56141/ag_ui_protocol-0.1.18-py3-none-any.whl", hash = "sha256:d151c0f0a34160647f1571163f7185746f4326b15a56d1560de5082a7a0e7a12", size = 12607, upload-time = "2026-04-21T20:45:00.097Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0a/bcad8116eb058e4b4a305e3fc37ebd7efc879deeb86b854f1c5b8b6e97dd/ag_ui_protocol-0.1.19-py3-none-any.whl", hash = "sha256:898843b1410d378824da0c6a776486288b9c5828689d0bf563118868e37f390f", size = 13490, upload-time = "2026-06-02T17:26:16.313Z" }, ] [[package]] @@ -68,36 +71,16 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.1" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "idna", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - -[[package]] -name = "anyio" -version = "4.13.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] @@ -111,238 +94,220 @@ wheels = [ [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, - { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, - { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, - { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, - { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, - { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, - { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, + { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.7" +version = "3.4.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, - { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, - { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, - { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, - { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, - { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, - { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, - { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, - { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, - { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] name = "click" -version = "8.3.3" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -356,62 +321,59 @@ wheels = [ [[package]] name = "cryptography" -version = "48.0.0" +version = "49.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, ] [[package]] @@ -435,63 +397,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] -[[package]] -name = "fastapi" -version = "0.128.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "annotated-doc", marker = "python_full_version < '3.10'" }, - { name = "pydantic", marker = "python_full_version < '3.10'" }, - { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, - { name = "typing-inspection", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, -] - [[package]] name = "fastapi" version = "0.139.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] dependencies = [ - { name = "annotated-doc", marker = "python_full_version >= '3.10'" }, - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "starlette", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" }, ] -[[package]] -name = "griffe" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, -] - [[package]] name = "griffelib" -version = "2.0.2" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] [[package]] @@ -521,8 +449,7 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, @@ -543,32 +470,17 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, @@ -576,118 +488,101 @@ wheels = [ [[package]] name = "jiter" -version = "0.14.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/2e/a9959997739c403378d0a4a3a1c4ed80b60aeace216c4d37b303a9fc60a4/jiter-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:02f36a5c700f105ac04a6556fe664a59037a2c200db3b7e88784fac2ddf02531", size = 316927, upload-time = "2026-04-10T14:25:40.753Z" }, - { url = "https://files.pythonhosted.org/packages/27/72/b6de8a531e0adbadd839bec301165feb1fccf00e9ff55073ba2dd20f0043/jiter-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41eab6c09ceffb6f0fe25e214b3068146edb1eda3649ca2aee2a061029c7ba2e", size = 321181, upload-time = "2026-04-10T14:25:42.621Z" }, - { url = "https://files.pythonhosted.org/packages/db/d8/2040b9efa13c917f855c40890ae4119fe02c25b7c7677d5b4fa820a851fc/jiter-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf4d4c109641f9cfaf4a7b6aebd51654e405cd00fa9ebbf87163b8b97b325aa", size = 347387, upload-time = "2026-04-10T14:25:44.212Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/655c0ad5ce6a8e90f9068c175b8a236877d753e460762b3183c136db1c5b/jiter-0.14.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80c7b41a628e6be2213ad0ece763c5f88aa5ee003fa394d58acaaee1f4b8342", size = 373083, upload-time = "2026-04-10T14:25:45.55Z" }, - { url = "https://files.pythonhosted.org/packages/f1/66/549c40fa068f08710b7570869c306a051eb67a29758bd64f4114f730554c/jiter-0.14.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb3dbf7cc0d4dbe73cce307ebe7eefa7f73a7d3d854dd119ea0c243f03e40927", size = 463639, upload-time = "2026-04-10T14:25:47.452Z" }, - { url = "https://files.pythonhosted.org/packages/25/2f/97a32a05fed14ed58a18e181fdfb619e05163f3726b54ee6080ec0539c09/jiter-0.14.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7054adcdeb06b46efd17b5734f75817a44a2d06d3748e36c3a023a1bb52af9ec", size = 380735, upload-time = "2026-04-10T14:25:49.305Z" }, - { url = "https://files.pythonhosted.org/packages/2a/3b/4347e1d6c2a973d653bbb7a2d671a2d2426e54b52ba735b8ff0d0a29b75c/jiter-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d597cd1bf6790376f3fffc7c708766e57301d99a19314824ea0ccc9c3c70e1e2", size = 358632, upload-time = "2026-04-10T14:25:50.931Z" }, - { url = "https://files.pythonhosted.org/packages/ef/24/ca452fbf2ea33548ed30ce68a39a50442d3f7c9bf0704a7af958a930c057/jiter-0.14.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:df63a14878da754427926281626fd3ee249424a186e25a274e78176d42945264", size = 359969, upload-time = "2026-04-10T14:25:52.381Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a3/94470a0d199287caabeb4da2bb2ae5f6d17f3cf05dfc975d7cb064d58e0f/jiter-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ea73187627bcc5810e085df715e8a99da8bdfd96a7eb36b4b4df700ba6d4c9c", size = 397529, upload-time = "2026-04-10T14:25:53.801Z" }, - { url = "https://files.pythonhosted.org/packages/cf/71/6768edc09d7c45c39f093feb3de105fa718a3e982b5208b8a2ed6382b44b/jiter-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9f541eaf7bb8382367a1a23d6fc3d6aad57f8dd8c18c3c17f838bee20f217220", size = 522342, upload-time = "2026-04-10T14:25:55.396Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6b/5c2e17559a0f4e96e934479f7137df46c939e983fa05244e674815befb73/jiter-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:107465250de4fce00fdb47166bcd51df8e634e049541174fe3c71848e44f52ce", size = 556784, upload-time = "2026-04-10T14:25:56.927Z" }, - { url = "https://files.pythonhosted.org/packages/b1/83/c25f3556a60fc74d11199100f1b6cc0c006b815c8494dea8ca16fe398732/jiter-0.14.0-cp310-cp310-win32.whl", hash = "sha256:ffb2a08a406465bb076b7cc1df41d833106d3cf7905076cc73f0cb90078c7d10", size = 208439, upload-time = "2026-04-10T14:25:58.796Z" }, - { url = "https://files.pythonhosted.org/packages/2e/99/781a1b413f0989b7f2ea203b094b331685f1a35e52e0a45e5d000ecaab27/jiter-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb8b682d10cb0cce7ff4c1af7244af7022c9b01ae16d46c357bdd0df13afb25d", size = 204558, upload-time = "2026-04-10T14:26:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, - { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, - { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, - { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, - { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, - { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, - { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, - { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, - { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, - { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, - { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, - { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, - { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, - { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, - { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, - { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, - { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, - { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, - { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, - { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, - { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, - { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, - { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, - { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, - { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, - { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, - { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, - { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, - { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, - { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, - { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, - { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, - { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, - { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, - { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, - { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, - { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, - { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, - { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, - { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, - { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, - { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a0/704f5027508138f22cda277779bc6b9cace77af1a87ef93c73daf61b5b12/jiter-0.14.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:85581c4c3e4060fe3424cdfd7f3aa610f2dc5e9dde8b6863358eb68560018472", size = 319926, upload-time = "2026-04-10T14:28:04.687Z" }, - { url = "https://files.pythonhosted.org/packages/54/f6/ee3ddf36a0989959760417fbe662e4f7bc6451547a54262ef17c2882875d/jiter-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c6279c63849444a4fe9b9abf82e5df0fc7d13dea07f53f084b362485bd1f2bbe", size = 315333, upload-time = "2026-04-10T14:28:06.248Z" }, - { url = "https://files.pythonhosted.org/packages/63/c1/ba033578d36c99455f5fbd8bbce891c29e2348dfa25811875a636975d82f/jiter-0.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59940ef6ac9f8b34c800838416f105f0503485fa8d71cae99f71d44a7285b01e", size = 349996, upload-time = "2026-04-10T14:28:07.807Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fd/b6c8b38d1c53bd115abdd3143038f6f29fa4eaad915205ca959e5a16cc08/jiter-0.14.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55bee2b6a2657434984d9144c20cf27ba3b6acd495539539953e447778515efd", size = 373255, upload-time = "2026-04-10T14:28:09.597Z" }, - { url = "https://files.pythonhosted.org/packages/e4/48/153044aee4ebc7e4c92be697dd96da8e8eb7f920e1e90e4dda0c5aa7e400/jiter-0.14.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d45fc7ea86a46bd9b5bceb9e8d43e5d10a392378713fb32cf1ce851b4b0d1f8", size = 466311, upload-time = "2026-04-10T14:28:11.563Z" }, - { url = "https://files.pythonhosted.org/packages/c0/7d/2d7f2d29543f8d2960396cc44068228ef9badaac35998b255277b439d3b4/jiter-0.14.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:758d19dae7ea4c4da3cbc463dc323d1660e7353144ef17509ff43beab6da5a47", size = 382612, upload-time = "2026-04-10T14:28:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/fa/8c/0522d2dac614f141111d80fb9ca1363251dcef796b21523d4a9f78684f88/jiter-0.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32959d7285d1d0deb5a8c913349e476ad9271b384f3e54cca1931c4075f54c6e", size = 361841, upload-time = "2026-04-10T14:28:14.736Z" }, - { url = "https://files.pythonhosted.org/packages/e8/48/53b5627bf5027fd9d49a35c5921e3c220140b51ef68814bfc93750c562b1/jiter-0.14.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:78a4c677fe5689e0e129b39f5affe9210a500b6620ebb0386ebccf5922bee9a6", size = 362913, upload-time = "2026-04-10T14:28:16.583Z" }, - { url = "https://files.pythonhosted.org/packages/7e/b5/5dde259eec978b54be123128311ee0e9eb4cc095019d06ad45401913567f/jiter-0.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ae66782ecffb1a266e1a07f5abbfc3832afdd260fc9b478982c3f8e01eba5fa", size = 399862, upload-time = "2026-04-10T14:28:18.779Z" }, - { url = "https://files.pythonhosted.org/packages/b6/0a/ef4bce553be8d9e2bbbdbdd3c0337c54773c8a9057c969242afa770d24d3/jiter-0.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:155dab67beac8d66cec9479c93ee2cbe7bfbc67509e5c2860e02ec2d9b0ecca1", size = 524733, upload-time = "2026-04-10T14:28:20.851Z" }, - { url = "https://files.pythonhosted.org/packages/4a/0b/e2416d657d1a9a2503f3edcb493cec5b770b8cb417ccab7ed013b56242c8/jiter-0.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f16b76d7d6aadbbaf7f79a76ff3a51dae14b7ebaaf9c1ba61607784ef51c537c", size = 558776, upload-time = "2026-04-10T14:28:22.85Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d0/aaed54dfa1c0375df395b2fc6cf7cfb86d480c1e38b821006207774c04de/jiter-0.14.0-cp39-cp39-win32.whl", hash = "sha256:0fbad7aa06f87e8215d660fc6f05a9b07b58751a29967bbd9c81ff22d21dbe8c", size = 211145, upload-time = "2026-04-10T14:28:24.654Z" }, - { url = "https://files.pythonhosted.org/packages/0b/19/1a7be527189cdd61694cd98b83fd382e614e52053bc992728d1cda55022e/jiter-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:e1765c3ef3ea31fe6e282376a16def1a96f5f11a0235055696c18d9d23ff30cb", size = 206779, upload-time = "2026-04-10T14:28:26.31Z" }, - { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, - { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, - { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, - { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, - { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" }, + { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" }, + { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, ] [[package]] @@ -695,10 +590,11 @@ name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "python_full_version >= '3.10'" }, - { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, - { name = "referencing", marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", marker = "python_full_version >= '3.10'" }, + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -710,7 +606,7 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing", marker = "python_full_version >= '3.10'" }, + { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ @@ -719,36 +615,35 @@ wheels = [ [[package]] name = "mcp" -version = "1.27.1" +version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "httpx", marker = "python_full_version >= '3.10'" }, - { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, - { name = "jsonschema", marker = "python_full_version >= '3.10'" }, - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "pydantic-settings", marker = "python_full_version >= '3.10'" }, - { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.10'" }, - { name = "python-multipart", marker = "python_full_version >= '3.10'" }, - { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, - { name = "starlette", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, - { name = "uvicorn", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] [[package]] name = "openai" -version = "2.37.0" +version = "2.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, { name = "distro" }, { name = "httpx" }, { name = "jiter" }, @@ -757,51 +652,27 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/32/50/5901f01ef14e6c27788beb91e54fef5d6204fb5fb9e97402fc8a14de2e32/openai-2.37.0.tar.gz", hash = "sha256:f4bc562cc5f3a43d40d678105572d9d44765f6e0f50c125f63055419b72f4bd9", size = 754706, upload-time = "2026-05-15T22:30:35.428Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/60/d4219875289b11d2c2f7da93c36283da224a2e55865ed865ab64e0ce9217/openai-2.45.0.tar.gz", hash = "sha256:10d34ca9c5643bce775852fddbfc172505cb1d4de1ccd101696c3ecff358765d", size = 1109653, upload-time = "2026-07-09T18:02:44.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/4c/bce61680d0699a78a405fd9a67989b175ba020590428831aab2ab1d2be7c/openai-2.37.0-py3-none-any.whl", hash = "sha256:814633888b8f3b1ffd6615697c6e4ef93632d08b7c2e28c8c5ef3556e5a10107", size = 1303238, upload-time = "2026-05-15T22:30:32.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b0/2291689e3ec4723fbf5bbf3b54afcd7b160f9ddc98ca7aedfd0132af5677/openai-2.45.0-py3-none-any.whl", hash = "sha256:5df105f5f8c9b711fcb9d06d2d3888cebc82506db216484c14a4e53cdf651777", size = 1629470, upload-time = "2026-07-09T18:02:42.21Z" }, ] [[package]] name = "openai-agents" -version = "0.8.4" +version = "0.18.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "griffe", marker = "python_full_version < '3.10'" }, - { name = "openai", marker = "python_full_version < '3.10'" }, - { name = "pydantic", marker = "python_full_version < '3.10'" }, - { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/e0/9fa9eac9baf2816bc63cee28967d35a7ed9dc2f25e9fd2004f48ed6c8820/openai_agents-0.8.4.tar.gz", hash = "sha256:5d4c4861aedd56a82b15c6ddf6c53031a39859a222f08bbd5645d5967efa05e8", size = 2389744, upload-time = "2026-02-11T19:14:30.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/dc/10df015aebb0797a8367aab65200ac4f5221df20bbae76930f5b6ac8e001/openai_agents-0.8.4-py3-none-any.whl", hash = "sha256:2383c6e8e59ed4146b89d1b6f53e34e55caf94bc14ae3fd704e7aad5021f4ff1", size = 380662, upload-time = "2026-02-11T19:14:28.864Z" }, -] - -[[package]] -name = "openai-agents" -version = "0.17.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", + { name = "griffelib" }, + { name = "mcp" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "websockets" }, ] -dependencies = [ - { name = "griffelib", marker = "python_full_version >= '3.10'" }, - { name = "mcp", marker = "python_full_version >= '3.10'" }, - { name = "openai", marker = "python_full_version >= '3.10'" }, - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "types-requests", version = "2.33.0.20260513", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, - { name = "websockets", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/cd/14acaf94c6a438cfe72c5ea043bfbc77d7fbb9514ab7796d82f2180d1518/openai_agents-0.17.2.tar.gz", hash = "sha256:5e11414bdd8c20c8e9192d21f78265d30e65992f4537f940309eca9255804449", size = 5403689, upload-time = "2026-05-12T03:14:57.433Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/0c/52e9aeff5549b225d5666a0eb84a8a22b4c47db08b6f44dbd45876fcfba3/openai_agents-0.18.2.tar.gz", hash = "sha256:9f418bb563eddff1e01f245ae8a4964b7649396f444b569b4113d105e41ca1d3", size = 5546139, upload-time = "2026-07-11T01:08:18.537Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/71/5ba9afa4b6a0d250bfdbdd1cf9a5255ae131a7f82915c634ac50c0633c3a/openai_agents-0.17.2-py3-none-any.whl", hash = "sha256:1b3560c1690bcee635a487f77ebfb8b4fb2dd52a653e045a86e51974ab87faf3", size = 838225, upload-time = "2026-05-12T03:14:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/9c/23/b5b6b80a3e36f021ca2a8c4637684f0d722d646b19d3616768a68802c302/openai_agents-0.18.2-py3-none-any.whl", hash = "sha256:c7aea341b256a90b87b17b7e444bab29a12655864a2f0094f65561223d867185", size = 874310, upload-time = "2026-07-11T01:08:16.851Z" }, ] [[package]] @@ -944,20 +815,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/5d/00/13a0c039569d1e583779ee1b8d7df6bfe275a0db83fcae14f01d6856c16e/pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae", size = 2115337, upload-time = "2026-05-06T13:38:37.741Z" }, - { url = "https://files.pythonhosted.org/packages/41/60/e70fa1ee03e243bdfd4b1fddf1e1f2a8fba681df3034b51b9376c0fb5bf5/pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201", size = 1957976, upload-time = "2026-05-06T13:37:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/11/9a/78fb5f2ea849f767ea802de8b4e8f5a0c4a48ddbe4bc66bd19ac2f55a01c/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0", size = 1979390, upload-time = "2026-05-06T13:36:52.419Z" }, - { url = "https://files.pythonhosted.org/packages/f5/7d/3acfdcd000bad9735de0430a88355948469781f62cb841fd63e8a307e80e/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15", size = 2043263, upload-time = "2026-05-06T13:39:54.798Z" }, - { url = "https://files.pythonhosted.org/packages/35/60/1325e5a8d7f9697416481c7f7c1c304738d6b961a7fd1ea0f054ce0f14fb/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76", size = 2225708, upload-time = "2026-05-06T13:40:24.887Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9ec8c38f33b26db0b612cb7fd165bb0a370773710432a2a74fa31287b430/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49", size = 2288494, upload-time = "2026-05-06T13:38:00.091Z" }, - { url = "https://files.pythonhosted.org/packages/65/05/497446a9586d1b2d24ee25ebe208beb15388f1875d783e1e014055d150ac/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928", size = 2095629, upload-time = "2026-05-06T13:38:23.632Z" }, - { url = "https://files.pythonhosted.org/packages/93/d9/cd5fa98f9d94f9294c15459396c8a2383c164469e679ac178d6d42cfee6b/pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066", size = 2119309, upload-time = "2026-05-06T13:39:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/20/1b/64cec655451ddbf3976df5dc9706b240df4fdaebdeebeadd4f59a8dab926/pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6", size = 2170216, upload-time = "2026-05-06T13:39:14.561Z" }, - { url = "https://files.pythonhosted.org/packages/2a/21/fe9f039138c9ea3be10ccdb6ec490acb54dcbef5a5e96dbdf1411f82b929/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9", size = 2186726, upload-time = "2026-05-06T13:37:51.597Z" }, - { url = "https://files.pythonhosted.org/packages/44/cb/19ca0da64821d1aefcef65f253aa9ecbdd0dde360f607d0f9b3d95db2b4e/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29", size = 2320400, upload-time = "2026-05-06T13:39:36.29Z" }, - { url = "https://files.pythonhosted.org/packages/cd/14/fe3fbf6e845bf2080dc2f282d75085ddf79d037b35634ecde68f33c217b4/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9", size = 2363318, upload-time = "2026-05-06T13:38:53.039Z" }, - { url = "https://files.pythonhosted.org/packages/62/88/60b110889507a426eecf626f7536566cb290ada71147eff49b6e2724ca62/pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1", size = 1988880, upload-time = "2026-05-06T13:39:16.572Z" }, - { url = "https://files.pythonhosted.org/packages/0b/d6/8ede2f98f17e1e4e127d37be0eced4eee931a511c62cd68af50e1b25bfa9/pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac", size = 2079257, upload-time = "2026-05-06T13:39:38.498Z" }, { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, @@ -978,16 +835,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] @@ -1001,57 +858,33 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.12.1" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [package.optional-dependencies] crypto = [ - { name = "cryptography", marker = "python_full_version >= '3.10'" }, -] - -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pluggy", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { name = "cryptography" }, ] [[package]] name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pluggy", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ @@ -1069,36 +902,36 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.28" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] [[package]] name = "pywin32" -version = "311" +version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, - { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] [[package]] @@ -1106,45 +939,25 @@ name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "certifi", marker = "python_full_version < '3.10'" }, - { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, - { name = "idna", marker = "python_full_version < '3.10'" }, - { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - [[package]] name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.10'" }, - { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, - { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ @@ -1155,6 +968,10 @@ wheels = [ name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, @@ -1274,57 +1091,167 @@ wheels = [ ] [[package]] -name = "sniffio" -version = "1.3.1" +name = "rpds-py" +version = "2026.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, ] [[package]] -name = "sse-starlette" -version = "3.4.4" +name = "sniffio" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "starlette", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] -name = "starlette" -version = "0.49.3" +name = "sse-starlette" +version = "3.4.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "anyio" }, + { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, + { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, ] [[package]] name = "starlette" -version = "1.0.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] dependencies = [ - { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] @@ -1383,53 +1310,23 @@ wheels = [ [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20260107" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, -] - -[[package]] -name = "types-requests" -version = "2.33.0.20260513" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] -dependencies = [ - { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/f7/3228dd3794941bcb92ca6ca2045a6671a828ec0b47becbef23310bc45559/types_requests-2.33.0.20260513.tar.gz", hash = "sha256:bd845450e954e751373d5d33526742592f298808a3ee3bda7e858e46b839b57f", size = 24714, upload-time = "2026-05-13T05:39:23.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/f5/233a78be8367a9888de718f002fb27b1ea4be39471cd88aedeafceed872e/types_requests-2.33.0.20260513-py3-none-any.whl", hash = "sha256:d5a965f9d18b6e06b72039a69565de9027e58f36a7f709857da747fbe7521122", size = 21390, upload-time = "2026-05-13T05:39:22.262Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] @@ -1444,25 +1341,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - [[package]] name = "urllib3" version = "2.7.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, @@ -1470,82 +1352,130 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.47.0" +version = "0.51.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "python_full_version >= '3.10'" }, - { name = "h11", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, ] [[package]] name = "websockets" -version = "16.0" +version = "16.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/02/b9a097e1e16fee4e2fd1ec8c39f6a9c5d6257bae8fa12640caf869f54436/websockets-16.1.tar.gz", hash = "sha256:299468cbe42e2b9981134c7c51d99387d8a7bf562b00183b3eec53f882846dad", size = 182530, upload-time = "2026-07-10T06:32:57.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, - { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, + { url = "https://files.pythonhosted.org/packages/8c/31/cd11d2796b95c93645bac8e396b0f4bac0896a07a7b87d473bfc359f02c3/websockets-16.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de72a9c611178b15557d98eabd3101c9663c4d68938510478a6d162f99afd213", size = 179772, upload-time = "2026-07-10T06:30:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/34306d802f9b599eab041688a2086318037560cfae616a860234cca575b6/websockets-16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:37b0e4d726ffea3776670092d3d13e1cb605076f036a695fd1259de0d9b9fe02", size = 177457, upload-time = "2026-07-10T06:30:24.636Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/36ebbb978a7af70ff952afe5b22561264967164e9ad68b6734cae94efeb4/websockets-16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:00d50c0a27098fcb7ab47b3d99a1b1159b534dbcd959fbf05113ebc37e5f927b", size = 177737, upload-time = "2026-07-10T06:30:25.954Z" }, + { url = "https://files.pythonhosted.org/packages/17/d7/944f341d0d3c0450ffd3d171479531df1818cb1df1623af4065113999c44/websockets-16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1acb698bff1da1782b31aebd8d7a24d7d05453964abcd7d03dbf6e25893908e8", size = 186244, upload-time = "2026-07-10T06:30:27.235Z" }, + { url = "https://files.pythonhosted.org/packages/32/e5/a9b98fc49ef0214718a9c839c6c63856a921877256ec46f371be32decfa8/websockets-16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc2c453f3b5f99c56b16e233aad5299860558487d26adb2ed27a00c14ca24b8c", size = 187484, upload-time = "2026-07-10T06:30:28.615Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7a/a575b52ca090b1976ffbe4b5f0762d03f399dfcb48eab883101331be71a9/websockets-16.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1a9f08a0728b0835f1c6abe1d9b746ab3de49b7336a0e1919cf96be1e76273eb", size = 190143, upload-time = "2026-07-10T06:30:29.91Z" }, + { url = "https://files.pythonhosted.org/packages/7c/40/705fbbd5677242fd36f724e9a94103e6bbdcb7d71e8f4498bfc1a8a7d413/websockets-16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a089979d6173b27af18026c8d8b0077f83669a9169174482c4651e9f5739a5b6", size = 188004, upload-time = "2026-07-10T06:30:31.357Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0c/58227c8d66b1c4060c53bac8e066fb4fe2603060408e934f48660a448d72/websockets-16.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3c18dba232ec2b92a68579c9fed8ff5a18f853d1e09fc0b6ca3159e94f689fe", size = 186689, upload-time = "2026-07-10T06:30:32.712Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d0/5c1314782594aa347e0f18808ee277a61986a2a2f9f470df9893183995bd/websockets-16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c1eb7df4170d5068892a8834fb5c07b9552353deb0dbeb0bff3820481ae4792", size = 184559, upload-time = "2026-07-10T06:30:34.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e6/109c6f16850fd674b7e3d0e58b8987f05d3881abaa25f42a9faf5e85f097/websockets-16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c522bd48e625b6d557aa228967258d6d3da031c4cc21d3352fb302479aa9ba0a", size = 186997, upload-time = "2026-07-10T06:30:35.397Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ec/6afa1aebc59426438b85cf7a3868c53a89005e2250a648c99e99943b90a4/websockets-16.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d106396927a7f00b0f3a69215c3357f87bf0bca6844247121f7e8291e826a3b1", size = 185621, upload-time = "2026-07-10T06:30:36.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/24/c038fe8682e9345bfa422d2cc5cc68b0491ab942c92e176bf8dfa6e8331f/websockets-16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d71bed12909b8039955536e192867d02d76cd3797cedfd0facf822e7668636c3", size = 187384, upload-time = "2026-07-10T06:30:38.096Z" }, + { url = "https://files.pythonhosted.org/packages/30/ca/dc0ef2be39c67394e24bc982a0af59cd6249bf2f4e4272813c5c505d0da9/websockets-16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9c1cf6f9a936b030b5bed0e800c5ee32069338129084546baf5ff5014dc62fa9", size = 185258, upload-time = "2026-07-10T06:30:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/3ffecd83ca3404b41fbdf8e9b178e55b529cd59bf64ea08b5a37b616b568/websockets-16.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3fd3e6a7af2c8fcdcf4ffbeaf7f54a567b91a83267204187797f31faaa2a4efa", size = 186050, upload-time = "2026-07-10T06:30:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/75/26/2e068497c78f31591a610ab7ef6d8d383ecadbe98f9121e1ebda77ef6d2b/websockets-16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dddd27175bf640acae5561fa79b77e8ec71fc445816200523e5c19b6a556fb72", size = 186273, upload-time = "2026-07-10T06:30:42.309Z" }, + { url = "https://files.pythonhosted.org/packages/44/ab/4dc049cb2c9e1be3a2c6fef77118f9c5049979e99cd56a97759d2f40f980/websockets-16.1-cp310-cp310-win32.whl", hash = "sha256:cce36c80b3f2fede7942f1756d3d885fa6fa086766c8c1bcf00695ab80f0d51a", size = 180157, upload-time = "2026-07-10T06:30:43.565Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/5e010ce5f66a8e5df380843f704ada508195a021c0c8a0f933639c9ee1c0/websockets-16.1-cp310-cp310-win_amd64.whl", hash = "sha256:115fc4695b94bb855995b23fb1abcb66099a5995575d3d5bc5605a616c58d0eb", size = 180458, upload-time = "2026-07-10T06:30:45.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d47429afcc2c28616c32640009c84ea3f95660dab805766345b9682468e0/websockets-16.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a9b1d7a63cba8e6b9b77e499a81eab29d31100298d090ad4507d1048c0b9cae0", size = 179770, upload-time = "2026-07-10T06:30:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/2f0a722039a1e0107be73ed672ba604449b4956e48733e8e6b8a005aea42/websockets-16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bedbc5efeb96621aa2921d2d92608246691399418cac22acba427eb11877ea1f", size = 177455, upload-time = "2026-07-10T06:30:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/43/6a/c26b0ae449e93d256ce5cdd50d5fe97b575a63e8dcd311a1faa972fd6bc6/websockets-16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd847ab82133015afe65d778e7966ab42dba16bd7ad2e5b8a7918db6539f3f94", size = 177731, upload-time = "2026-07-10T06:30:49.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3f/381550b344a02f0d2f84cda25e79b54575291bc7022128a41163fe8ba5b0/websockets-16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2fb33ccb16ee40a95cc676d7b0ff451a9a2632f11a0dbc2e666326892b2e1de", size = 187066, upload-time = "2026-07-10T06:30:50.505Z" }, + { url = "https://files.pythonhosted.org/packages/4a/87/5ab1ec2086910f23cfb9ec0c1c29fbcc24a9d190b5198b1557c00ce4a47e/websockets-16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f15b6d9ea9c2eaf6ccab964a082b09bfa6634a495bb0c2e9e7ee6943f58976", size = 188301, upload-time = "2026-07-10T06:30:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/75/4b/bbbb8e6fac4cfc53d7aaa69a3d531bf10799354b0021f4b58914aced8c1a/websockets-16.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:638cf57c48b4ad8ac1ff1e453f4f97db2426b690ddc111e6da96b27b4a340bc3", size = 191594, upload-time = "2026-07-10T06:30:53.229Z" }, + { url = "https://files.pythonhosted.org/packages/5c/da/6c0c349443d6e999f481e3d9a0e57e7ac2956d75d6391bec24b92af3fe13/websockets-16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c1c85f61bc9d5eac57ce705d848dc2d2ce3680638300bf4e1da7d749e2cf4ce", size = 188862, upload-time = "2026-07-10T06:30:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ea/a368d37c010425a5451f42052fe804e754e23333e8448aef5d55c8a8d64f/websockets-16.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:eeab6d27f51c7e579023c971f5e6dff200deadf01faf6831beaecd32052dfaef", size = 187633, upload-time = "2026-07-10T06:30:56.055Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4e/2ecd59add10d0855ec03dbdedfcdacdbd1aaabcd44b7dcbeda27538662e9/websockets-16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ed64e5a97b0b97a0b66e18bfe281317a75fbbd5afe692f939ea8d14a4292f2c", size = 185089, upload-time = "2026-07-10T06:30:57.444Z" }, + { url = "https://files.pythonhosted.org/packages/6f/eb/c6c3dcd7a01097bb0d42f4e9ef21a2c2a491d36b77cd0870ab59f9e8e77f/websockets-16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b3b021d0ed4bc16eea9775f62c9fa71acdacba0fc790b38581754dedf29ca60", size = 187790, upload-time = "2026-07-10T06:30:58.731Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3e/775d36885d5e48ab8020aaf377de0ff5fbeb8bc2682a7e46419e4a14521c/websockets-16.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6eb604a4167f0a0d53c2243dfc667a29f0b43c3436057184e070bb82a1000fa2", size = 186381, upload-time = "2026-07-10T06:31:00.355Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/6305c00812a92e47d0582604c02bd759db0118bbafc13f707d712dbcf898/websockets-16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9a3f125e44c3e34d61d111652e608e0f5b85ce08c225c8d56ad0eb822fa40030", size = 188193, upload-time = "2026-07-10T06:31:01.677Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/96bf8302c81d961585b4d34a2ddd3f229782f9b8c57bc78bbf98f1b1a4ac/websockets-16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8fdf0b00d0d1f30d1f06a92cab46fe542eec3eb302a7aee7163f142d0780f216", size = 185771, upload-time = "2026-07-10T06:31:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1f/e8fe44b1d2dc417d740d9959d28fd2a846f268e7df38a686c04ac7dfe947/websockets-16.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67b56828712f5fa7852de4c0265c28827311a657a4d275b7312ed0d1a918bee4", size = 186803, upload-time = "2026-07-10T06:31:04.34Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/b07d3a4e1eb2ab03e94e7f53f0c7a628e85fde6ad86011f7afd08f27b985/websockets-16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39c7e7730be33b8f0cd6f0aa8e8c82f9cdd1813f159765e073b2ece65f4824b5", size = 187041, upload-time = "2026-07-10T06:31:05.567Z" }, + { url = "https://files.pythonhosted.org/packages/a6/fd/e0abb8acc435642ac4a671490f6cf781c882f3fe682cdced9080ea455ab5/websockets-16.1-cp311-cp311-win32.whl", hash = "sha256:c54fe94fb2f11e11b48920c5f971e298cec73ac35db56efe57a49db63dfc95d4", size = 180158, upload-time = "2026-07-10T06:31:06.929Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/85574d9458d3b913090087b817df0cc47b68e9a01dd0ab6ac04b77f49b0a/websockets-16.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9f4fb9ae8b802e55609685db98382d48fd3feb1397804e1e774968dea0f28c7", size = 180456, upload-time = "2026-07-10T06:31:08.247Z" }, + { url = "https://files.pythonhosted.org/packages/a1/52/748c014f07f4e0e170c8932de7e647a1511d5ab3049cd978797136aee577/websockets-16.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b6aa3f7ad345cf3862c21f4fbf2ef5e14d911348476c2845e137c091fe3a3f0b", size = 179798, upload-time = "2026-07-10T06:31:09.664Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5e/2a2e64d977d084e49d37c187c26c056daaff41965be7300cd5dbde6f8b07/websockets-16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b43fcfb521ac2f34ba80b7b8ea16303e4ad82dd8af667bf40839ad3a5d37b164", size = 177478, upload-time = "2026-07-10T06:31:11.072Z" }, + { url = "https://files.pythonhosted.org/packages/aa/12/5b85b4e75d697e548a94962ce5c036b05dd21cb9545759d555c5586422fc/websockets-16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bd3e12cd9afbe2baedae0b1eeade8ba64329b60fe2f9abdc966bd10fd2c2ef5", size = 177746, upload-time = "2026-07-10T06:31:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/9d/62/79b1c8f0cee0da648b4899e1c5b0dbd3aa59846985136a54854db6827ab4/websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a", size = 187345, upload-time = "2026-07-10T06:31:13.754Z" }, + { url = "https://files.pythonhosted.org/packages/25/34/b7c5c52c2f24280e1c017acb7ad491a566750a5cceca7f3cf999373bba21/websockets-16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a24d1f35aef07d794a16c853c688e74956c50239bec37b4f2de080056046419b", size = 188581, upload-time = "2026-07-10T06:31:15.075Z" }, + { url = "https://files.pythonhosted.org/packages/bc/37/604193bebcbeffe96fdf795960b83a15d600880c64dc17ec9c31c5b3427d/websockets-16.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c64c024ddf7a35331b21fcddb562a039c275d2c82e8c2d12939e7da23997270", size = 191362, upload-time = "2026-07-10T06:31:16.395Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b4/5ee27575b367d7110d4d13945e2a9de067ec84dc71e54b87f01e38550d9a/websockets-16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e99757f5baafe20fc598e202ea6f5b0b265186ad38d0a17bd8beca16296955", size = 189216, upload-time = "2026-07-10T06:31:17.776Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/3e2dcc78d85fc5d9d814895ce6d07d0dfacc0f6aaa1d151f2b8c8d772299/websockets-16.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:353f3bc6e058ac1ccab4b3588e8598837a8c04cfc8351233e6d523be675d844c", size = 187971, upload-time = "2026-07-10T06:31:19.152Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2f/cd271717b93d5ee19626cb5e38a85baab745c86e33db7c31a3ac729b31b8/websockets-16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0352f5b38b40e857b6428d468fa21dbb4dd4a567d933c26d9831b4efe1b92f43", size = 185381, upload-time = "2026-07-10T06:31:20.665Z" }, + { url = "https://files.pythonhosted.org/packages/78/91/6ad6f2f1426317b5001bd490534208c7360636b35bac1dec2e0c22bfc40e/websockets-16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70bd789afab579602968c39f21cb925466505f3edff22f0ae852bca54978a4f9", size = 188015, upload-time = "2026-07-10T06:31:22.024Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6d/533733132ab4c07540efd4a8f0b9a435d3a5059b2f26cc476ace1abf7f45/websockets-16.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d0fb4b46f121eccd539353baebd1083a8767a9a351109453d1d1caecd1ba40c2", size = 186619, upload-time = "2026-07-10T06:31:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/08/73/16c059f3d73b3331eba10793704afa4faa9939234fb08ef7dca35794e8f0/websockets-16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c14b6634af01541e4efe2954fd8f263386f7aa6d37c01e55dd8109fd17661452", size = 188497, upload-time = "2026-07-10T06:31:25.024Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/9a8fae7dd2acdcfb1a8844c29fe42b518a04b64fce38a0923b6290e452f1/websockets-16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a58532c49a851bcb481e58c1be23b315c17fe2fbbed509d75aeea12f543d2c15", size = 186051, upload-time = "2026-07-10T06:31:26.291Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/b240c7dd6a0e0c59c1f68377cc3015263521080c327c15f5e753c1f6d378/websockets-16.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4e969170c3b08e1d8dabd990fef1fa702c4233aeaabec33f871806e444f6a0e4", size = 187029, upload-time = "2026-07-10T06:31:27.605Z" }, + { url = "https://files.pythonhosted.org/packages/50/35/524e3fac40e47d6fdcf6c4b2c95ef1bc8a97e01593c90eff86621df7b716/websockets-16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff9b000064b88787ba9f7a3cb2af2b68a658ca5aad76458a46469e7124b678a0", size = 187308, upload-time = "2026-07-10T06:31:28.927Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/56840cf62c8859af6ba22b9529da937332468c80f32b598753e8a66d3990/websockets-16.1-cp312-cp312-win32.whl", hash = "sha256:b9f5d83f80f4d7c4bba6d97f3755ac05850c784dce0fd2ab371c4e41172f53ff", size = 180161, upload-time = "2026-07-10T06:31:30.316Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ff/87eb9eb44cb62424a8d729834f2b0515a47e2669fabec29820268f4d50a1/websockets-16.1-cp312-cp312-win_amd64.whl", hash = "sha256:6852c9f653966c16109d3b6f31181fd734f7914927e3f0fa1117af7a18c9aa21", size = 180462, upload-time = "2026-07-10T06:31:31.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/63/df158b155420b566f025e75613424ad9649a24bcb0e9f259321ab3d58bea/websockets-16.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b0232ed141cec3df2af5a3959a071c51f40036336b0d37e17faf9ef52fc73e47", size = 179791, upload-time = "2026-07-10T06:31:33.108Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/00fe9414dfeafa6fe54eae9f5716c8c8e9ac59d192be3b893c096d395846/websockets-16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a71b73d143991714144e159f767b698f03c4a70b8a65ae1733b650cff488045b", size = 177472, upload-time = "2026-07-10T06:31:34.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/76/b10633424d40681b4e892ffd08ca5226322b2426e62d4ab71eae484c3a32/websockets-16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:187323204c3b2fc465e8fc2609e60437c521790cb9c1acb49c4c452a33e57f37", size = 177737, upload-time = "2026-07-10T06:31:35.964Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/d3bb03b2229bb1afd72008742d586cf1ea240dce64dd48c71c8c7fd3294c/websockets-16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dba74233c8c3ce368850818c98354dad2570f57231b3fd3bd00d7aa57628881", size = 187403, upload-time = "2026-07-10T06:31:37.496Z" }, + { url = "https://files.pythonhosted.org/packages/26/16/cc2e80478f688fc3c39c67dc1fac6a0783858058914ebc2489917462cb42/websockets-16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63339bc8c63c86a463177775cb7c677691f5bcfac7b3b2f01b286d42acd41600", size = 188639, upload-time = "2026-07-10T06:31:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/15/d6/ad87b2507e57de1cbf897a56c963f2925962ed5e85fbe06aaa83ced27acd/websockets-16.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23e545ea8ae4263e37cdfd4e22a217f519e48e432728bc461185bbf585f38a83", size = 190078, upload-time = "2026-07-10T06:31:40.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1a/5b37b3fd335d5811f29fc829f2646a3e6d1463a4bf09c3100708684c766e/websockets-16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2237081454846fb40403a80ba86d82e2038b9c45865ab96af0abe7d002a91045", size = 189267, upload-time = "2026-07-10T06:31:41.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/98/06afc33e9450d4230f94c664db78875d90f5f6a5fb77f0bc6ec15ae74e1c/websockets-16.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f5218de1ed047385ca53744caba9435d65f75d008364970a3fae95a05812cf9", size = 188022, upload-time = "2026-07-10T06:31:42.838Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/42fef5d5887c18cf2d148b02debf56cecb9cfbffc68027cde9b12c8f432c/websockets-16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75c98e3920039d0edff03b74478ada504b7ce3a1bc406db2cabfca84320f7baf", size = 185435, upload-time = "2026-07-10T06:31:44.219Z" }, + { url = "https://files.pythonhosted.org/packages/a0/9b/8021c133add5fe40ed40312553a6cd1408c069d7efe3444ad483d4973ed3/websockets-16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1facd189d8190af30487a55b4c3688484dd50801628a3b5b2ccd26db08e67057", size = 188080, upload-time = "2026-07-10T06:31:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/69/54/1e37384f395eaa127383aab15c1c45e200890a7d7b99db5c312233d193e0/websockets-16.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cc0c6a6eef613c7da32d4fb068f82ef834b58134f6a16b54e6c1e5bf9529ab3d", size = 186678, upload-time = "2026-07-10T06:31:47.449Z" }, + { url = "https://files.pythonhosted.org/packages/68/79/1caeacab5bc2081e4519288d248bc8bd2de30652e6eaa94be6be09a1fe5b/websockets-16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ad9411eded8988b879be6038206698bf7106c85a78f642c004485bcb95be17eb", size = 188554, upload-time = "2026-07-10T06:31:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/83/b3dca5fad71487b726e31cb0acf56f226792c1cc34e6ab18cbf146bd2d74/websockets-16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cd68f0914f3b64694895bc5e9b14e8b447e41d7bf5ffaf989bb8dcb5e2dfdce7", size = 186109, upload-time = "2026-07-10T06:31:50.508Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0b/8f246c3712f07f207b52ea5fb47f3b2b66fafec7303162644c74aed51c6a/websockets-16.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fef2debfe7f7ebdda12176f26166f95b7af17af05ba06150fcf889032e0213e9", size = 187061, upload-time = "2026-07-10T06:31:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/47/eb/27d6c92a01696b6495386af4fc941d7d0a13f2eab2bf9c336111d7321491/websockets-16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3cd6c9b798218798f4bb7b2e71c38f0e744bb94ca537b13376f88019d46384d", size = 187347, upload-time = "2026-07-10T06:31:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d5/eeee439921f55d5eaeabcea18d0f7ce32cdc39cb8fc1e185431a094c5c7b/websockets-16.1-cp313-cp313-win32.whl", hash = "sha256:84c170c6869633536921e4474b1cce7254c0c9b0053ef5725f966cee47e718e4", size = 180149, upload-time = "2026-07-10T06:31:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/a3/03/971e98d4a4864cf263f9e94c5b2b7c9a9b7682d77bfbba4e732c55ee85a9/websockets-16.1-cp313-cp313-win_amd64.whl", hash = "sha256:bef52d327d70fa75dad93ee61ea2cb1d1489aca9f35c188833563f5a3b4df0a5", size = 180458, upload-time = "2026-07-10T06:31:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e6/da1dc11507f8118145a81c751fe0c77e5e1c11b8554496addb39389e2dc2/websockets-16.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f881fca0a45dd6789939bd6637cd98169b92f1c3fdc78262f2cb9ec2cb1f324e", size = 179833, upload-time = "2026-07-10T06:31:58.19Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/c0d46f62e31e232487b2c123bc3cfd9a4e45684ca7dc0c37f0987f29baae/websockets-16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c379d5b207d3a7f0ba4c2e4602a895b0bcc63fb5f5371a4ae7fbddb03b672b", size = 177524, upload-time = "2026-07-10T06:31:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/4a/33/abd966074b34a51e4f134e0aaed80f5a4a0a35163ea5ac58a1bc5a076d23/websockets-16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98ab58a4faa72b46da0127ccc1931dcbfc0985b0778892300a092185910c4cbe", size = 177743, upload-time = "2026-07-10T06:32:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/ea/30/646e47b8a8dff04e227bdab512e6dde60663a647eeac7bbd6edddd92bbc5/websockets-16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e9c4e369fc181b2d41a99e01477215cecdc8546a39f7d41a59cc0a7065a0b09", size = 187474, upload-time = "2026-07-10T06:32:02.54Z" }, + { url = "https://files.pythonhosted.org/packages/d2/72/890ab9d77494af93ea65268230bfbc0a90ba789401ed7a44356a44785644/websockets-16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0704df094b2d5fa7f6f410925a594c2a5c9a09167731a76292e5410934208209", size = 188717, upload-time = "2026-07-10T06:32:04.156Z" }, + { url = "https://files.pythonhosted.org/packages/d5/aa/baedbbaa6bf9ed6029617ed5e8976535bd805f483ca9b3484e7ad9ee08bf/websockets-16.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b22b1f4950f6ab7126623329c3b47b3b90a14c05db517f2db2a026ad6c928352", size = 190090, upload-time = "2026-07-10T06:32:05.822Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/d813ec94e18002571ef4959d87a630eff6e01b72a51bcb0832b75ae8c51a/websockets-16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1ae4a686a662964a6671069f84f7f908cc3475e782227726b0c622c715962105", size = 189320, upload-time = "2026-07-10T06:32:07.223Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3c/8ec52a6662f3df64090fba28cd521d405d54759268d8e820477037e8c80d/websockets-16.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:856bdd638f8277f86465057bfdd4da097c73058fb0f9d2bd5baea29e2bf2d367", size = 188068, upload-time = "2026-07-10T06:32:08.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/7f/f0ae6042b14f86fa5f996c6563ea4cf107adc036ccbedc9d4f418d0095f9/websockets-16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9003a1fde1c21a322a3ca3fa0c4bda8c639da81dbc925162766086643b05ba87", size = 185493, upload-time = "2026-07-10T06:32:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/5ffc53af9939c49fd653d147fa5b8f78ced1f6bce6c49a7446860945b0ce/websockets-16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39e947b1f5fdab045174306e3916785bf3ed537648acc1549827c08c33b10953", size = 188141, upload-time = "2026-07-10T06:32:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/67/62/729206c0ee577a4db8eae6dd06e0eef725a1287c6df11b2ef831d003df31/websockets-16.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5dd0e666b5931c0509cf65714686a1c5126771e663a79ac5d40da4f58b1f9502", size = 186653, upload-time = "2026-07-10T06:32:12.845Z" }, + { url = "https://files.pythonhosted.org/packages/1b/86/e8806a99ec4589914f255e6b658853fe537bf359c05e6ba5762ad9c27917/websockets-16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a0285df7925657ad65a65fb8dc330808bce082827538fd50ef45fa12d1fc5bca", size = 188614, upload-time = "2026-07-10T06:32:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/89/38/ac554e2fc6ff0b8deeff9798b92e7abd8f99e2bd9731532e7033de208220/websockets-16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:82d1c2cab3c133e9d059b3a5420bed9376bd30e21c185c63dda4ddadf6ddda47", size = 186165, upload-time = "2026-07-10T06:32:15.626Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c5/4ef4d8e53342f94f3c49e1ae089b32c1e8b3878e15e0022c7708c647f351/websockets-16.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c39907f1eaf11f6277def65aa02d68f30576b693d0c1ca332aafa3caa723ac6d", size = 187119, upload-time = "2026-07-10T06:32:17.114Z" }, + { url = "https://files.pythonhosted.org/packages/3a/33/4788b1dd417bd97eeb2698af3b9df6775ac656f96e9987da0419a067602f/websockets-16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:45c5ea55446171949eb99fd34b771ceddd511ca21958d40d0197ced33159e5ee", size = 187411, upload-time = "2026-07-10T06:32:18.629Z" }, + { url = "https://files.pythonhosted.org/packages/30/38/00d37aad6dc3244ce349e2864815362e50b3cfc00cac28d216db20efe40f/websockets-16.1-cp314-cp314-win32.whl", hash = "sha256:b8ef8b1c8d6bd029a475ac432e730fba2dfd456715d26c473e2a82291024b99c", size = 179822, upload-time = "2026-07-10T06:32:20.233Z" }, + { url = "https://files.pythonhosted.org/packages/9d/37/2a8cb0eaddee5eaebda47a90a3ba0898d1ce3d866b02a4857fea17d82e5b/websockets-16.1-cp314-cp314-win_amd64.whl", hash = "sha256:7358ff21632b5d062707f73e859c824f1c3807e73d8ca25e71caca7c4cdcf145", size = 180167, upload-time = "2026-07-10T06:32:21.749Z" }, + { url = "https://files.pythonhosted.org/packages/07/5a/262ad5fcaef4198997b165060f09a63f861e76939b1786ab546ccc3f8120/websockets-16.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d0f38f4c3e9b359e257c339c2cc1967ccaeedb102e57c1c986bdce4bf4f32268", size = 180166, upload-time = "2026-07-10T06:32:23.278Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/36377db690f4292826e4501a6dec2801dc55fd1cf0405923b04937e478df/websockets-16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3c3d2cbd1602593bad49bd86fa3fbb25407d87a3b4bf8857c0ac5ac4914e1901", size = 177697, upload-time = "2026-07-10T06:32:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c7/07171abce1e39799a76f473608580fe98bd43a1230f5146159622c02bccf/websockets-16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:36069b74671e7e667f48a7484249f84c45a825a134c8b1bdc01875d0daa10d79", size = 177902, upload-time = "2026-07-10T06:32:26.564Z" }, + { url = "https://files.pythonhosted.org/packages/14/17/c831f48e250bc4749f57c00dcce73337c41cd32f6d59a64567b84e782601/websockets-16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:587f83c2ce8a5d628e166384d77fa7f0ac69b9007d515ab442123e6615aa8da3", size = 187766, upload-time = "2026-07-10T06:32:27.981Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2e/4dfe63e245b0ecfaf470cf082d25c6ce35808159135fd88c82653a6b11ab/websockets-16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6db7972d52bc1b66cefe2246902e256cbaebc9ba8a45eac09343d7eb6671b2", size = 188939, upload-time = "2026-07-10T06:32:29.365Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/5faf65aebd9562f6b4bc473d24ce38cc56f84eb5f5bee66ed9b86733f93c/websockets-16.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e7d6014888a0632e1ed7a4095248bb3095232999447f2d83bfb1900987dd9ed9", size = 191081, upload-time = "2026-07-10T06:32:30.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/cd/2634f2f2c0556c1aae6501ed6840019cc569dd6fdbcac6494378daea4dc0/websockets-16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cb074d150e4ad2a77aa8a332c2be85f3f64f2681519d2570c1225c12c9821ff", size = 189513, upload-time = "2026-07-10T06:32:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/2c700b51196104f09715b326b1f092ed25326bdf79a03e00a4842e503743/websockets-16.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d19c9067e1fe9490f974bffbc0e443b80a7674c5efb4980c429cc00771f07c5a", size = 188240, upload-time = "2026-07-10T06:32:33.897Z" }, + { url = "https://files.pythonhosted.org/packages/f1/20/86283636e499a1a357fa9441f690ba34f255e731f2fea174132b3b762b57/websockets-16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d440ff0c6c7469ad59c0a412c383c235935b43635e89425e3f6a0c36de90c31b", size = 185955, upload-time = "2026-07-10T06:32:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/91/23/d7fb734b0095d43bc7f1c9f68afd50adb4176e7e513403e8c70ad7daa4fa/websockets-16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8613129a2533f08de24505e69a3e403cedaadae49abdb043c4d170ca71b7e4bd", size = 188491, upload-time = "2026-07-10T06:32:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5e/168a192689db468405ecf3b8e4a2c18811936b0724d017ad7e6d252734f0/websockets-16.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a5bf9c23f197b4ec88290fd5463f33db67362a1bb10f85fc2e8e7627f0ddab97", size = 186983, upload-time = "2026-07-10T06:32:38.207Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9b/66795fa91ebe49019ebe4fa910282172252e37046b80e08fc52e0c365150/websockets-16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:520b0fd0395f075febb283c76755af724ab9fd19dffa4f3bfd18cb4e622790a3", size = 188890, upload-time = "2026-07-10T06:32:39.545Z" }, + { url = "https://files.pythonhosted.org/packages/5a/32/126bbc844be5afb3613fd43211dac10a9645f4cf39741d04acaa2ec7030c/websockets-16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7143aa09a67e1c013be44e81a88dfe90fc6244198ab86c7edd064152cf619805", size = 186583, upload-time = "2026-07-10T06:32:41.038Z" }, + { url = "https://files.pythonhosted.org/packages/22/b9/0b5db9cbcf6e4970db4496893244a8d92e07f71a8ef27cf34b08aa02fef1/websockets-16.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:7acb811fad08e611755800d1560e395c67e11a6bd563598ea6abb319afb86938", size = 187353, upload-time = "2026-07-10T06:32:42.501Z" }, + { url = "https://files.pythonhosted.org/packages/99/2e/254b2131a10d831b76e2c18dfe7add9729c6292c674a8085bf8de01ad151/websockets-16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c5cf88e3faa2f7931bc6baeee7599c97656a3f6ac7f831f4fccba233e141783a", size = 187784, upload-time = "2026-07-10T06:32:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/21/dc/e7288aa8e3ac5a88a0924619984d663c1abf2a87d0ea98290c66fdaee0ec/websockets-16.1-cp314-cp314t-win32.whl", hash = "sha256:589f8842521c8307684ce0b40ce4ad70c5e0aa46484c6f1225a94ef4b8970341", size = 179947, upload-time = "2026-07-10T06:32:45.495Z" }, + { url = "https://files.pythonhosted.org/packages/d3/de/37edf1260ff0fbbd2f82433489c4cfbe799ac2ff21355331609879329fe6/websockets-16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c0e0857c30bbbc2bb5c30687508f0b7ec19aa026cd9f2ff8424d0fee42dcc07", size = 180291, upload-time = "2026-07-10T06:32:47.119Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f4/84ef884775bbe77c46cce79bc7d705ea3bc6574cc00acf81af89754c077d/websockets-16.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7289d899c79e763e6221c8dcb8959361cb43274418538d7c7ad16a43b01d12f9", size = 177387, upload-time = "2026-07-10T06:32:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d9/6831ec6f65e1eeac770375f4f4b604f23df9bafaa1b47004bc5f9488d513/websockets-16.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e22e9e3719f5131bd62da4db63c8da63eb8c91cc99e16c1cbd122f130e1ae07a", size = 177663, upload-time = "2026-07-10T06:32:50.043Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/21d4922fa7fe855813a8b38f181a0ecf02a586e16c1f095fd05471f78cc2/websockets-16.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83bdabafef431247e6b11a9aab8a0893fd8e82e1ed95b32e0373625b03ffce4a", size = 178501, upload-time = "2026-07-10T06:32:51.439Z" }, + { url = "https://files.pythonhosted.org/packages/91/87/7a0320df854dacd09507ca972cb04a4dc5aae279583cc5b80ad5f5819533/websockets-16.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b8d13ceabc5c60995f201b5211d76876e17e68706ebf5d3bc666b32eefff1a6", size = 179397, upload-time = "2026-07-10T06:32:52.892Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/0da1eb8c8da2ace7b578c8523d32618af85e62a9ebad56051d4a14a38a1c/websockets-16.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81495f9c0085361c582efbc3207fb877174cfe03370f17d9cd70624404aa526f", size = 180546, upload-time = "2026-07-10T06:32:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/66/58/bd83247f39ddc26ffc2c24eb05087a3b749e00cb4509fc6d19daa23c8495/websockets-16.1-py3-none-any.whl", hash = "sha256:c5149dfe490ec7e5ee5dbf624c642fb725f93a5575c7f00ab594ca9eddb8dd81", size = 174031, upload-time = "2026-07-10T06:32:56.079Z" }, ] From 4bcfe57eaadaa5db9439a1b27907bb5658276883 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 21:58:33 +0200 Subject: [PATCH 39/94] feat(examples): update Python version requirement to 3.10 and rename package to ag-ui-openai-agents --- .../python/examples/pyproject.toml | 6 +- .../openai-agents/python/examples/uv.lock | 724 ++---------------- 2 files changed, 87 insertions(+), 643 deletions(-) diff --git a/integrations/openai-agents/python/examples/pyproject.toml b/integrations/openai-agents/python/examples/pyproject.toml index 1b1f4b0e51..d57c2e2073 100644 --- a/integrations/openai-agents/python/examples/pyproject.toml +++ b/integrations/openai-agents/python/examples/pyproject.toml @@ -4,16 +4,16 @@ version = "0.1.0" description = "Example usage of the AG-UI × OpenAI Agents SDK integration" license = "MIT" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ - "ag-ui-openai-agent-sdk", + "ag-ui-openai-agents", "ag-ui-protocol>=0.1.18", "fastapi>=0.115.2", "uvicorn[standard]>=0.35.0", ] [tool.uv.sources] -ag-ui-openai-agent-sdk = { path = "../", editable = true } +ag-ui-openai-agents = { path = "../", editable = true } [project.scripts] dev = "server:main" diff --git a/integrations/openai-agents/python/examples/uv.lock b/integrations/openai-agents/python/examples/uv.lock index 7ca556581f..f1a7a5cc2e 100644 --- a/integrations/openai-agents/python/examples/uv.lock +++ b/integrations/openai-agents/python/examples/uv.lock @@ -1,27 +1,25 @@ version = 1 revision = 3 -requires-python = ">=3.9" +requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", - "python_full_version < '3.10'", + "python_full_version < '3.11' and sys_platform != 'win32'", ] [[package]] -name = "ag-ui-openai-agent-sdk" +name = "ag-ui-openai-agents" version = "0.1.0" source = { editable = "../" } dependencies = [ { name = "ag-ui-protocol" }, - { name = "fastapi", version = "0.128.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "fastapi", version = "0.139.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "openai-agents", version = "0.8.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "openai-agents", version = "0.17.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fastapi" }, + { name = "openai-agents" }, { name = "pydantic" }, + { name = "uvicorn" }, ] [package.metadata] @@ -30,6 +28,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.115.12" }, { name = "openai-agents", specifier = ">=0.8.4" }, { name = "pydantic", specifier = ">=2.13.4" }, + { name = "uvicorn", specifier = ">=0.51.0" }, ] [package.metadata.requires-dev] @@ -40,17 +39,15 @@ name = "ag-ui-openai-agents-examples" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "ag-ui-openai-agent-sdk" }, + { name = "ag-ui-openai-agents" }, { name = "ag-ui-protocol" }, - { name = "fastapi", version = "0.128.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "fastapi", version = "0.139.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, - { name = "uvicorn", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, + { name = "fastapi" }, + { name = "uvicorn", extra = ["standard"] }, ] [package.metadata] requires-dist = [ - { name = "ag-ui-openai-agent-sdk", editable = "../" }, + { name = "ag-ui-openai-agents", editable = "../" }, { name = "ag-ui-protocol", specifier = ">=0.1.18" }, { name = "fastapi", specifier = ">=0.115.2" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" }, @@ -86,39 +83,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] -[[package]] -name = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "idna", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - [[package]] name = "anyio" version = "4.14.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ @@ -148,7 +120,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -223,18 +195,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, - { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, - { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, - { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, - { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, - { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, - { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, ] [[package]] @@ -339,54 +299,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, - { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, - { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, - { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, - { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, - { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, - { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, - { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, - { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, - { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - [[package]] name = "click" version = "8.4.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ @@ -407,8 +328,8 @@ name = "cryptography" version = "49.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } wheels = [ @@ -473,68 +394,29 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] -[[package]] -name = "fastapi" -version = "0.128.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "annotated-doc", marker = "python_full_version < '3.10'" }, - { name = "pydantic", marker = "python_full_version < '3.10'" }, - { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, - { name = "typing-inspection", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, -] - [[package]] name = "fastapi" version = "0.139.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "annotated-doc", marker = "python_full_version >= '3.10'" }, - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "starlette", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" }, ] -[[package]] -name = "griffe" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, -] - [[package]] name = "griffelib" version = "2.1.0" @@ -614,13 +496,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/707b05d0a75f22ab301ff2660ebd4c2567cb13496ce5c277cafe8fa847a7/httptools-0.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8", size = 210820, upload-time = "2026-05-25T22:17:40.382Z" }, - { url = "https://files.pythonhosted.org/packages/05/5b/1f9b7462464294db5d0b4e0fcb285c2f8233fb29ce48141c26b40fd505f3/httptools-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5", size = 114585, upload-time = "2026-05-25T22:17:41.314Z" }, - { url = "https://files.pythonhosted.org/packages/8a/52/037b6e734eaf5395d552fdc7459b7d0affaa33df07c5c6c7e02d60f6331c/httptools-0.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b", size = 451355, upload-time = "2026-05-25T22:17:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/31/d0/8d09dcac561cd23050133e887b219e9361be9f547d3616db66b5857ed91a/httptools-0.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72", size = 452250, upload-time = "2026-05-25T22:17:43.911Z" }, - { url = "https://files.pythonhosted.org/packages/17/c5/c11ac814a89052dc0dba5ff99009f447e2e46ddb37eaa72d24079675ee9e/httptools-0.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005", size = 437132, upload-time = "2026-05-25T22:17:44.95Z" }, - { url = "https://files.pythonhosted.org/packages/35/e4/33ebdb8acb9650661966b3ca5687158122bf43c48207747afcc0245f66d8/httptools-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247", size = 438395, upload-time = "2026-05-25T22:17:46.465Z" }, - { url = "https://files.pythonhosted.org/packages/06/f6/e0577ea0f86af402772f363c7f9ba321c9ed8c760d223749c51365b162e2/httptools-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62", size = 91214, upload-time = "2026-05-25T22:17:47.61Z" }, ] [[package]] @@ -628,8 +503,7 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, @@ -746,19 +620,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, - { url = "https://files.pythonhosted.org/packages/5e/60/489a9df48730f05cfe4fb71ed2ed805264f60d3b0bbc65816f1cd8a4372a/jiter-0.16.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d8f80521644426d451e70f00c7974240cab8f6ee088aedaa9af2697153ab7805", size = 312477, upload-time = "2026-06-29T13:04:39.423Z" }, - { url = "https://files.pythonhosted.org/packages/69/79/7b84250429f0e262b935c627a8522cab31b5303e617400b14e808b599bdb/jiter-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b21b412b899fd8bd51a3046934b59a3bb068b79f70a5c6010053ac77cc53f0c", size = 315397, upload-time = "2026-06-29T13:04:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/14/d2/a52aa9786bbec7e85547f041efec59b517488c396958cbf432d703898a73/jiter-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0758ab7747a984797cf048e8eedea1d8ef39d7994b25611daf5b48fc903e8873", size = 344340, upload-time = "2026-06-29T13:04:42.96Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f0/f33bfac2598a7e256e994b02444835ddd934226df7b022ae588c536131ec/jiter-0.16.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ec553a99b0987efd7a3645a1a825cf29c224e494db267a83369fcc8da9aeda5", size = 368451, upload-time = "2026-06-29T13:04:44.451Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f1/7fd3bd9378ff4366032a17fc82f562dda8aa92c491451705dc8c35275683/jiter-0.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3bd327cdfa118bc1ce69c214c2678571d5bd39b8ccd0ebf43a54db00541ba9a", size = 466402, upload-time = "2026-06-29T13:04:45.877Z" }, - { url = "https://files.pythonhosted.org/packages/ab/14/6d9948c7da0d56fe3a786a950b0f4a03139160e59238f2327361d6dfbda7/jiter-0.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26d122613ada2b708eb714695446f40fce5bdf2edb4b02116dec62faa62dfab3", size = 377987, upload-time = "2026-06-29T13:04:47.33Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c0/7acc77d44050cb3cc7ea4ddb32cc46614ba705876857973ebc2f008b1383/jiter-0.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e03a5f21a5ce96a9441b8cb32719a8b88ed5388f53e0f339c5bcf54f1317f9d0", size = 350341, upload-time = "2026-06-29T13:04:48.936Z" }, - { url = "https://files.pythonhosted.org/packages/26/a9/36e0d4af0f414ad054f6619f48ac7d607f42cb98f1672c39f964630556d0/jiter-0.16.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:a5c54ef4ff776d9675837ef535b3308d6e31c208d43ebc44a0f7ab8a208c68f7", size = 359924, upload-time = "2026-06-29T13:04:50.608Z" }, - { url = "https://files.pythonhosted.org/packages/27/7c/ecbe98e370db5f8196ecbdae34806d369a0df95404de1562eb8c68415985/jiter-0.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1e7923093a376d93c6eb507c77045ae258d689ba577392846a1b3f10d0b09a9", size = 397728, upload-time = "2026-06-29T13:04:52.336Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1d/eb4261cdf94d4a485a47e3544ff1ee4e8a752b24aedeae250dc00a238166/jiter-0.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2a0d46ef67cc58d906a6132dd3040ca70ae4f0b0d7c9c052fe432c658a69b3f6", size = 524713, upload-time = "2026-06-29T13:04:53.864Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c3/a7dd14c86bcd04e7b0d5d13937e980f94085b61c0eec12622eaa159965c4/jiter-0.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:70a490b55634dc0d2606ce8a8e01b1d62459011beb368d15d76e1eaf62460e3d", size = 555324, upload-time = "2026-06-29T13:04:55.364Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0a/a10f28b2038bc3492de03ad6b7126a678d9f9f928ae72968e0cc2427bdad/jiter-0.16.0-cp39-cp39-win32.whl", hash = "sha256:9acf1b2faec82d998811ecce7ae84d9005e53410773e9d37d61cdc424ba4581b", size = 209348, upload-time = "2026-06-29T13:04:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/9b6377e067ebb0cde1f751e28cab26b09d4c8809050935196ee32525e2c9/jiter-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:491e7d072a253b156fff46b78bceac4652a697aa8d7082c9c18c03d7b7917d24", size = 202012, upload-time = "2026-06-29T13:04:58.836Z" }, { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, @@ -774,10 +635,10 @@ name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "python_full_version >= '3.10'" }, - { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, - { name = "referencing", marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } @@ -790,7 +651,7 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing", marker = "python_full_version >= '3.10'" }, + { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ @@ -802,20 +663,20 @@ name = "mcp" version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "httpx", marker = "python_full_version >= '3.10'" }, - { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, - { name = "jsonschema", marker = "python_full_version >= '3.10'" }, - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "pydantic-settings", marker = "python_full_version >= '3.10'" }, - { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.10'" }, - { name = "python-multipart", marker = "python_full_version >= '3.10'" }, - { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, - { name = "starlette", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, - { name = "uvicorn", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } wheels = [ @@ -827,8 +688,7 @@ name = "openai" version = "2.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, { name = "distro" }, { name = "httpx" }, { name = "jiter" }, @@ -842,46 +702,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/f4/561ed79fd94876160018a5e75254cfcb9b0e62d4dded9dcb20072e86d623/openai-2.44.0-py3-none-any.whl", hash = "sha256:0a2a3ab2e29aeda368700f662ff9ba0f9df17ba4c54577a64e08b8115a3cc0ad", size = 1366216, upload-time = "2026-06-24T20:55:58.882Z" }, ] -[[package]] -name = "openai-agents" -version = "0.8.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "griffe", marker = "python_full_version < '3.10'" }, - { name = "openai", marker = "python_full_version < '3.10'" }, - { name = "pydantic", marker = "python_full_version < '3.10'" }, - { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "types-requests", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/e0/9fa9eac9baf2816bc63cee28967d35a7ed9dc2f25e9fd2004f48ed6c8820/openai_agents-0.8.4.tar.gz", hash = "sha256:5d4c4861aedd56a82b15c6ddf6c53031a39859a222f08bbd5645d5967efa05e8", size = 2389744, upload-time = "2026-02-11T19:14:30.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/dc/10df015aebb0797a8367aab65200ac4f5221df20bbae76930f5b6ac8e001/openai_agents-0.8.4-py3-none-any.whl", hash = "sha256:2383c6e8e59ed4146b89d1b6f53e34e55caf94bc14ae3fd704e7aad5021f4ff1", size = 380662, upload-time = "2026-02-11T19:14:28.864Z" }, -] - [[package]] name = "openai-agents" version = "0.17.7" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "griffelib", marker = "python_full_version >= '3.10'" }, - { name = "mcp", marker = "python_full_version >= '3.10'" }, - { name = "openai", marker = "python_full_version >= '3.10'" }, - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, - { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "griffelib" }, + { name = "mcp" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "websockets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d4/b2/235cbfdefe86623fc77f65fc1d016686372f61d4a1bf3fc66151de2eb847/openai_agents-0.17.7.tar.gz", hash = "sha256:ca76e7f882c9d8f06e3dfb8064cc33bcb5a5f34a29816cb9af863f395964ff0c", size = 5485068, upload-time = "2026-06-24T05:15:33.705Z" } wheels = [ @@ -1010,20 +842,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/5d/00/13a0c039569d1e583779ee1b8d7df6bfe275a0db83fcae14f01d6856c16e/pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae", size = 2115337, upload-time = "2026-05-06T13:38:37.741Z" }, - { url = "https://files.pythonhosted.org/packages/41/60/e70fa1ee03e243bdfd4b1fddf1e1f2a8fba681df3034b51b9376c0fb5bf5/pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201", size = 1957976, upload-time = "2026-05-06T13:37:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/11/9a/78fb5f2ea849f767ea802de8b4e8f5a0c4a48ddbe4bc66bd19ac2f55a01c/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0", size = 1979390, upload-time = "2026-05-06T13:36:52.419Z" }, - { url = "https://files.pythonhosted.org/packages/f5/7d/3acfdcd000bad9735de0430a88355948469781f62cb841fd63e8a307e80e/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15", size = 2043263, upload-time = "2026-05-06T13:39:54.798Z" }, - { url = "https://files.pythonhosted.org/packages/35/60/1325e5a8d7f9697416481c7f7c1c304738d6b961a7fd1ea0f054ce0f14fb/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76", size = 2225708, upload-time = "2026-05-06T13:40:24.887Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9ec8c38f33b26db0b612cb7fd165bb0a370773710432a2a74fa31287b430/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49", size = 2288494, upload-time = "2026-05-06T13:38:00.091Z" }, - { url = "https://files.pythonhosted.org/packages/65/05/497446a9586d1b2d24ee25ebe208beb15388f1875d783e1e014055d150ac/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928", size = 2095629, upload-time = "2026-05-06T13:38:23.632Z" }, - { url = "https://files.pythonhosted.org/packages/93/d9/cd5fa98f9d94f9294c15459396c8a2383c164469e679ac178d6d42cfee6b/pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066", size = 2119309, upload-time = "2026-05-06T13:39:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/20/1b/64cec655451ddbf3976df5dc9706b240df4fdaebdeebeadd4f59a8dab926/pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6", size = 2170216, upload-time = "2026-05-06T13:39:14.561Z" }, - { url = "https://files.pythonhosted.org/packages/2a/21/fe9f039138c9ea3be10ccdb6ec490acb54dcbef5a5e96dbdf1411f82b929/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9", size = 2186726, upload-time = "2026-05-06T13:37:51.597Z" }, - { url = "https://files.pythonhosted.org/packages/44/cb/19ca0da64821d1aefcef65f253aa9ecbdd0dde360f607d0f9b3d95db2b4e/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29", size = 2320400, upload-time = "2026-05-06T13:39:36.29Z" }, - { url = "https://files.pythonhosted.org/packages/cd/14/fe3fbf6e845bf2080dc2f282d75085ddf79d037b35634ecde68f33c217b4/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9", size = 2363318, upload-time = "2026-05-06T13:38:53.039Z" }, - { url = "https://files.pythonhosted.org/packages/62/88/60b110889507a426eecf626f7536566cb290ada71147eff49b6e2724ca62/pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1", size = 1988880, upload-time = "2026-05-06T13:39:16.572Z" }, - { url = "https://files.pythonhosted.org/packages/0b/d6/8ede2f98f17e1e4e127d37be0eced4eee931a511c62cd68af50e1b25bfa9/pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac", size = 2079257, upload-time = "2026-05-06T13:39:38.498Z" }, { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, @@ -1047,9 +865,9 @@ name = "pydantic-settings" version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ @@ -1061,7 +879,7 @@ name = "pyjwt" version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ @@ -1070,33 +888,13 @@ wheels = [ [package.optional-dependencies] crypto = [ - { name = "cryptography", marker = "python_full_version >= '3.10'" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, + { name = "cryptography" }, ] [[package]] name = "python-dotenv" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, @@ -1134,9 +932,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, - { url = "https://files.pythonhosted.org/packages/56/a0/f4a082a0232aaa7d0db2fe78b3e3ce03477ff8231c861b4405932b401001/pywin32-312-cp39-cp39-win32.whl", hash = "sha256:d620900033cc7531e50727c3c8333091df5dd3ffe6d68cdca38c03f5821408d5", size = 6360761, upload-time = "2026-06-04T07:49:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/b1/1f/d462540ddfccfea5ffe67ced40a99776f21d817fe6aeed5ab2cd87c1d926/pywin32-312-cp39-cp39-win_amd64.whl", hash = "sha256:dc90147579a905b8635e1b0ec6514967dcb07e6e0d9c42f1477feef14cac23bb", size = 6926456, upload-time = "2026-06-04T07:49:09.623Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6f/45e0e1d4078944440e7ee6fe003a39e412d53ab8a1772f0c3f4b2467b34c/pywin32-312-cp39-cp39-win_arm64.whl", hash = "sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc", size = 4306706, upload-time = "2026-06-04T07:49:11.793Z" }, ] [[package]] @@ -1201,15 +996,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, - { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, - { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, - { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, - { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, - { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, ] [[package]] @@ -1217,51 +1003,25 @@ name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "certifi", marker = "python_full_version < '3.10'" }, - { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, - { name = "idna", marker = "python_full_version < '3.10'" }, - { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - [[package]] name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.10'" }, - { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, - { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ @@ -1273,8 +1033,8 @@ name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ @@ -1537,45 +1297,21 @@ name = "sse-starlette" version = "3.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "starlette", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, + { name = "starlette" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, ] -[[package]] -name = "starlette" -version = "0.49.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, -] - [[package]] name = "starlette" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ @@ -1594,18 +1330,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, ] -[[package]] -name = "types-requests" -version = "2.32.4.20260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, -] - [[package]] name = "typing-extensions" version = "4.16.0" @@ -1627,30 +1351,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - [[package]] name = "urllib3" version = "2.7.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, @@ -1658,63 +1362,26 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.39.0" +version = "0.51.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "h11", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/4f/f9fdac7cf6dd79790eb165639b5c452ceeabc7bbabbba4569155470a287d/uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302", size = 82001, upload-time = "2025-12-21T13:05:17.973Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/25/db2b1c6c35bf22e17fe5412d2ee5d3fd7a20d07ebc9dac8b58f7db2e23a0/uvicorn-0.39.0-py3-none-any.whl", hash = "sha256:7beec21bd2693562b386285b188a7963b06853c0d006302b3e4cfed950c9929a", size = 68491, upload-time = "2025-12-21T13:05:16.291Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "httptools", marker = "python_full_version < '3.10'" }, - { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pyyaml", marker = "python_full_version < '3.10'" }, - { name = "uvloop", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles", version = "1.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "websockets", version = "15.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] - -[[package]] -name = "uvicorn" -version = "0.50.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "click", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "h11", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/41/06cce5dbb9f77591512957710ac709e60b12e6216a2f2d0d607fd49706e8/uvicorn-0.50.0.tar.gz", hash = "sha256:0c92e1bc2259cb7faa4fcef774a5966588f2e88542744550b66799fba10b76f1", size = 93257, upload-time = "2026-07-04T05:03:26.33Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/3a/eb70620ca2bf8213603d5c731460687c49fee38b0072f0b4a637781f0a53/uvicorn-0.50.0-py3-none-any.whl", hash = "sha256:05f0eb19edf38208f79f43df8a63081b48df31b0cd1e5997be957a4dc97d1b19", size = 72716, upload-time = "2026-07-04T05:03:24.848Z" }, + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, ] [package.optional-dependencies] standard = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "httptools", marker = "python_full_version >= '3.10'" }, - { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pyyaml", marker = "python_full_version >= '3.10'" }, - { name = "uvloop", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, ] [[package]] @@ -1759,150 +1426,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, - { url = "https://files.pythonhosted.org/packages/bd/1b/6fbd611aeba01ef802c5876c94d7be603a9710db055beacbad39e75a31aa/uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4", size = 1345858, upload-time = "2025-10-16T22:17:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/9e/91/2c84f00bdbe3c51023cc83b027bac1fe959ba4a552e970da5ef0237f7945/uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c", size = 743913, upload-time = "2025-10-16T22:17:12.165Z" }, - { url = "https://files.pythonhosted.org/packages/cc/10/76aec83886d41a88aca5681db6a2c0601622d0d2cb66cd0d200587f962ad/uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54", size = 3635818, upload-time = "2025-10-16T22:17:13.812Z" }, - { url = "https://files.pythonhosted.org/packages/d5/9a/733fcb815d345979fc54d3cdc3eb50bc75a47da3e4003ea7ada58e6daa65/uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659", size = 3685477, upload-time = "2025-10-16T22:17:15.307Z" }, - { url = "https://files.pythonhosted.org/packages/83/fb/bee1eb11cc92bd91f76d97869bb6a816e80d59fd73721b0a3044dc703d9c/uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743", size = 3496128, upload-time = "2025-10-16T22:17:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/76/ee/3fdfeaa9776c0fd585d358c92b1dbca669720ffa476f0bbe64ed8f245bd7/uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7", size = 3602565, upload-time = "2025-10-16T22:17:17.755Z" }, -] - -[[package]] -name = "watchfiles" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, - { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, - { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, - { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, - { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, - { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, - { url = "https://files.pythonhosted.org/packages/a4/68/a7303a15cc797ab04d58f1fea7f67c50bd7f80090dfd7e750e7576e07582/watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", size = 409220, upload-time = "2025-10-14T15:05:51.917Z" }, - { url = "https://files.pythonhosted.org/packages/99/b8/d1857ce9ac76034c053fa7ef0e0ef92d8bd031e842ea6f5171725d31e88f/watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", size = 396712, upload-time = "2025-10-14T15:05:53.437Z" }, - { url = "https://files.pythonhosted.org/packages/41/7a/da7ada566f48beaa6a30b13335b49d1f6febaf3a5ddbd1d92163a1002cf4/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", size = 451462, upload-time = "2025-10-14T15:05:54.742Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b2/7cb9e0d5445a8d45c4cccd68a590d9e3a453289366b96ff37d1075aaebef/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", size = 460811, upload-time = "2025-10-14T15:05:55.743Z" }, - { url = "https://files.pythonhosted.org/packages/04/9d/b07d4491dde6db6ea6c680fdec452f4be363d65c82004faf2d853f59b76f/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", size = 490576, upload-time = "2025-10-14T15:05:56.983Z" }, - { url = "https://files.pythonhosted.org/packages/56/03/e64dcab0a1806157db272a61b7891b062f441a30580a581ae72114259472/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", size = 597726, upload-time = "2025-10-14T15:05:57.986Z" }, - { url = "https://files.pythonhosted.org/packages/5c/8e/a827cf4a8d5f2903a19a934dcf512082eb07675253e154d4cd9367978a58/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", size = 474900, upload-time = "2025-10-14T15:05:59.378Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/94fed0b346b85b22303a12eee5f431006fae6af70d841cac2f4403245533/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", size = 457521, upload-time = "2025-10-14T15:06:00.419Z" }, - { url = "https://files.pythonhosted.org/packages/c4/64/bc3331150e8f3c778d48a4615d4b72b3d2d87868635e6c54bbd924946189/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", size = 632191, upload-time = "2025-10-14T15:06:01.621Z" }, - { url = "https://files.pythonhosted.org/packages/e4/84/f39e19549c2f3ec97225dcb2ceb9a7bb3c5004ed227aad1f321bf0ff2051/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", size = 623923, upload-time = "2025-10-14T15:06:02.671Z" }, - { url = "https://files.pythonhosted.org/packages/0e/24/0759ae15d9a0c9c5fe946bd4cf45ab9e7bad7cfede2c06dc10f59171b29f/watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", size = 274010, upload-time = "2025-10-14T15:06:03.779Z" }, - { url = "https://files.pythonhosted.org/packages/7e/3b/eb26cddd4dfa081e2bf6918be3b2fc05ee3b55c1d21331d5562ee0c6aaad/watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", size = 289090, upload-time = "2025-10-14T15:06:04.821Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, - { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, - { url = "https://files.pythonhosted.org/packages/00/db/38a2c52fdbbfe2fc7ffaaaaaebc927d52b9f4d5139bba3186c19a7463001/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", size = 409210, upload-time = "2025-10-14T15:06:14.492Z" }, - { url = "https://files.pythonhosted.org/packages/d1/43/d7e8b71f6c21ff813ee8da1006f89b6c7fff047fb4c8b16ceb5e840599c5/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", size = 397286, upload-time = "2025-10-14T15:06:16.177Z" }, - { url = "https://files.pythonhosted.org/packages/1f/5d/884074a5269317e75bd0b915644b702b89de73e61a8a7446e2b225f45b1f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", size = 451768, upload-time = "2025-10-14T15:06:18.266Z" }, - { url = "https://files.pythonhosted.org/packages/17/71/7ffcaa9b5e8961a25026058058c62ec8f604d2a6e8e1e94bee8a09e1593f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", size = 458561, upload-time = "2025-10-14T15:06:19.323Z" }, ] [[package]] name = "watchfiles" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } wheels = [ @@ -2014,97 +1545,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, ] -[[package]] -name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, - { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, - { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, - { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/36/db/3fff0bcbe339a6fa6a3b9e3fbc2bfb321ec2f4cd233692272c5a8d6cf801/websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5", size = 175424, upload-time = "2025-03-05T20:02:56.505Z" }, - { url = "https://files.pythonhosted.org/packages/46/e6/519054c2f477def4165b0ec060ad664ed174e140b0d1cbb9fafa4a54f6db/websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a", size = 173077, upload-time = "2025-03-05T20:02:58.37Z" }, - { url = "https://files.pythonhosted.org/packages/1a/21/c0712e382df64c93a0d16449ecbf87b647163485ca1cc3f6cbadb36d2b03/websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b", size = 173324, upload-time = "2025-03-05T20:02:59.773Z" }, - { url = "https://files.pythonhosted.org/packages/1c/cb/51ba82e59b3a664df54beed8ad95517c1b4dc1a913730e7a7db778f21291/websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770", size = 182094, upload-time = "2025-03-05T20:03:01.827Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0f/bf3788c03fec679bcdaef787518dbe60d12fe5615a544a6d4cf82f045193/websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb", size = 181094, upload-time = "2025-03-05T20:03:03.123Z" }, - { url = "https://files.pythonhosted.org/packages/5e/da/9fb8c21edbc719b66763a571afbaf206cb6d3736d28255a46fc2fe20f902/websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054", size = 181397, upload-time = "2025-03-05T20:03:04.443Z" }, - { url = "https://files.pythonhosted.org/packages/2e/65/65f379525a2719e91d9d90c38fe8b8bc62bd3c702ac651b7278609b696c4/websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee", size = 181794, upload-time = "2025-03-05T20:03:06.708Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/31ac2d08f8e9304d81a1a7ed2851c0300f636019a57cbaa91342015c72cc/websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed", size = 181194, upload-time = "2025-03-05T20:03:08.844Z" }, - { url = "https://files.pythonhosted.org/packages/98/72/1090de20d6c91994cd4b357c3f75a4f25ee231b63e03adea89671cc12a3f/websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880", size = 181164, upload-time = "2025-03-05T20:03:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/2d/37/098f2e1c103ae8ed79b0e77f08d83b0ec0b241cf4b7f2f10edd0126472e1/websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411", size = 176381, upload-time = "2025-03-05T20:03:12.77Z" }, - { url = "https://files.pythonhosted.org/packages/75/8b/a32978a3ab42cebb2ebdd5b05df0696a09f4d436ce69def11893afa301f0/websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4", size = 176841, upload-time = "2025-03-05T20:03:14.367Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, - { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, - { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, - { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/b7/48/4b67623bac4d79beb3a6bb27b803ba75c1bdedc06bd827e465803690a4b2/websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940", size = 173106, upload-time = "2025-03-05T20:03:29.404Z" }, - { url = "https://files.pythonhosted.org/packages/ed/f0/adb07514a49fe5728192764e04295be78859e4a537ab8fcc518a3dbb3281/websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e", size = 173339, upload-time = "2025-03-05T20:03:30.755Z" }, - { url = "https://files.pythonhosted.org/packages/87/28/bd23c6344b18fb43df40d0700f6d3fffcd7cef14a6995b4f976978b52e62/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9", size = 174597, upload-time = "2025-03-05T20:03:32.247Z" }, - { url = "https://files.pythonhosted.org/packages/6d/79/ca288495863d0f23a60f546f0905ae8f3ed467ad87f8b6aceb65f4c013e4/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b", size = 174205, upload-time = "2025-03-05T20:03:33.731Z" }, - { url = "https://files.pythonhosted.org/packages/04/e4/120ff3180b0872b1fe6637f6f995bcb009fb5c87d597c1fc21456f50c848/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f", size = 174150, upload-time = "2025-03-05T20:03:35.757Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c3/30e2f9c539b8da8b1d76f64012f3b19253271a63413b2d3adb94b143407f/websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123", size = 176877, upload-time = "2025-03-05T20:03:37.199Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, -] - [[package]] name = "websockets" version = "16.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and sys_platform != 'win32'", -] sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, From fa7d6a66a1e5cf6aecaa4e0f25cf61624cf37386 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 21:59:16 +0200 Subject: [PATCH 40/94] feat(openai-agents): optimize imports for python files --- .../examples/agents_examples/__init__.py | 2 +- .../agents_examples/ag_ui_docs_copilot.py | 6 ++-- .../custom_lifecycle_events.py | 2 +- .../agents_examples/dynamic_system_prompt.py | 2 +- .../human_in_the_loop_approval.py | 4 +-- .../python/examples/translator_server.py | 5 ++-- .../python/src/ag_ui_openai_agents/agent.py | 2 +- .../src/ag_ui_openai_agents/endpoint.py | 1 - .../ag_ui_openai_agents/engine/agui_to_sdk.py | 6 ++-- .../ag_ui_openai_agents/engine/sdk_to_agui.py | 30 +++++++++---------- .../src/ag_ui_openai_agents/engine/types.py | 3 +- .../openai-agents/python/tests/test_agent.py | 8 ++--- .../python/tests/test_endpoint.py | 2 +- .../python/tests/test_engine_mapping.py | 2 +- .../python/tests/test_multimodal.py | 1 - .../python/tests/test_snapshot.py | 14 ++++----- .../python/tests/test_translators.py | 6 ++-- 17 files changed, 46 insertions(+), 50 deletions(-) diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py index ea759eb3d7..045bb6f10e 100644 --- a/integrations/openai-agents/python/examples/agents_examples/__init__.py +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -21,8 +21,8 @@ from typing import Callable from agents import Agent -from ag_ui.core import CustomEvent +from ag_ui.core import CustomEvent from .agentic_chat import create_agentic_chat_agent from .backend_tool_rendering import create_backend_tool_agent from .custom_lifecycle_events import ( diff --git a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py index 936302ac2b..f5bd729edf 100644 --- a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py +++ b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py @@ -4,17 +4,15 @@ from pathlib import Path -from ag_ui.core import RunAgentInput -from ag_ui.encoder import EventEncoder from agents import Agent, Runner from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse +from ag_ui.core import RunAgentInput +from ag_ui.encoder import EventEncoder from ag_ui_openai_agents import AGUITranslator - from .constants import DEFAULT_MODEL - DOCS = Path(__file__).resolve().parents[2].joinpath("README.md").read_text(encoding="utf-8") # Documentation specialist: knows ag-ui-openai-agents docs diff --git a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py index 784604eeea..b520399022 100644 --- a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py +++ b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py @@ -19,10 +19,10 @@ import random -from ag_ui.core import CustomEvent, EventType from agents import Agent from fastapi import FastAPI +from ag_ui.core import CustomEvent, EventType from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint from .constants import DEFAULT_MODEL diff --git a/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py b/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py index 5cfb2c875c..da750444ff 100644 --- a/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py +++ b/integrations/openai-agents/python/examples/agents_examples/dynamic_system_prompt.py @@ -7,9 +7,9 @@ from __future__ import annotations from agents import Agent, RunContextWrapper -from ag_ui.core import Context from fastapi import FastAPI +from ag_ui.core import Context from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint from .constants import DEFAULT_MODEL diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py index 34d2dfa5cc..aa4ed5b4d1 100644 --- a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py @@ -36,11 +36,11 @@ from __future__ import annotations from agents import Agent, Runner, function_tool -from ag_ui.core import CustomEvent, EventType, RunAgentInput -from ag_ui.encoder import EventEncoder from fastapi import FastAPI from fastapi.responses import StreamingResponse +from ag_ui.core import CustomEvent, EventType, RunAgentInput +from ag_ui.encoder import EventEncoder from ag_ui_openai_agents import AGUITranslator from .constants import DEFAULT_MODEL diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index 31f6684fac..a3cc45dae0 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -63,13 +63,12 @@ import uvicorn from agents import Runner -from ag_ui.core import CustomEvent, EventType, RunAgentInput -from ag_ui.encoder import EventEncoder from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse +from ag_ui.core import CustomEvent, EventType, RunAgentInput +from ag_ui.encoder import EventEncoder from ag_ui_openai_agents import AGUITranslator - from agents_examples import DemoConfig, build_registry logging.basicConfig(level=logging.INFO) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index 58e349a05d..6ed083627f 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -9,8 +9,8 @@ from typing import Any, AsyncIterator, Callable from agents import Agent, RunConfig, Runner -from ag_ui.core import BaseEvent, CustomEvent, RunAgentInput +from ag_ui.core import BaseEvent, CustomEvent, RunAgentInput from .translator import AGUITranslator diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py index b31a4395f1..c286009226 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py @@ -13,7 +13,6 @@ from ag_ui.core import RunAgentInput from ag_ui.encoder import EventEncoder - from .agent import OpenAIAgentsAgent logger = logging.getLogger(__name__) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py index 0ef712c979..994c1b4f53 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py @@ -11,6 +11,9 @@ import logging from typing import Any, Iterable +from agents import FunctionTool, RunContextWrapper, TResponseInputItem +from agents.exceptions import AgentsException + from ag_ui.core import ( ActivityMessage, AssistantMessage, @@ -30,9 +33,6 @@ UserMessage, VideoInputContent, ) -from agents import FunctionTool, RunContextWrapper, TResponseInputItem -from agents.exceptions import AgentsException - from .helpers import coerce_to_str, read_attr from .types import TranslatedInput diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py index 331895d041..f62824e76c 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py @@ -15,6 +15,21 @@ import logging from typing import Any, Sequence +from agents import ( + HandoffCallItem, + HandoffOutputItem, + ItemHelpers, + MCPApprovalRequestItem, + MCPApprovalResponseItem, + MessageOutputItem, + ReasoningItem, + RunItem, + ToolCallItem, + ToolCallOutputItem, +) +from agents.items import MCPListToolsItem # not re-exported at package top level +from agents.models.fake_id import FAKE_RESPONSES_ID + from ag_ui.core import ( AssistantMessage, BaseEvent, @@ -42,21 +57,6 @@ ToolCallStartEvent, ToolMessage, ) -from agents import ( - HandoffCallItem, - HandoffOutputItem, - ItemHelpers, - MCPApprovalRequestItem, - MCPApprovalResponseItem, - MessageOutputItem, - ReasoningItem, - RunItem, - ToolCallItem, - ToolCallOutputItem, -) -from agents.items import MCPListToolsItem # not re-exported at package top level -from agents.models.fake_id import FAKE_RESPONSES_ID - from .helpers import ( coerce_to_str, new_message_id, diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index e25edbcb64..ed94688dcc 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -10,9 +10,10 @@ from typing import Any from agents import FunctionTool, TResponseInputItem -from ag_ui.core import Context from pydantic import BaseModel, ConfigDict, SkipValidation +from ag_ui.core import Context + class TranslatedInput(BaseModel): """SDK-ready bundle produced by translating an AG-UI RunAgentInput. diff --git a/integrations/openai-agents/python/tests/test_agent.py b/integrations/openai-agents/python/tests/test_agent.py index 71e36c71d9..beb7b7f11f 100644 --- a/integrations/openai-agents/python/tests/test_agent.py +++ b/integrations/openai-agents/python/tests/test_agent.py @@ -19,6 +19,10 @@ from unittest.mock import MagicMock import pytest +from agents import Agent, RunConfig +from agents.result import RunResultStreaming + +import ag_ui_openai_agents.agent as agent_module from ag_ui.core import ( CustomEvent, Context, @@ -27,11 +31,7 @@ Tool, UserMessage, ) -from agents import Agent, RunConfig -from agents.result import RunResultStreaming - from ag_ui_openai_agents import OpenAIAgentsAgent -import ag_ui_openai_agents.agent as agent_module def _run_input(with_tool: bool = False) -> RunAgentInput: diff --git a/integrations/openai-agents/python/tests/test_endpoint.py b/integrations/openai-agents/python/tests/test_endpoint.py index bfadf2d41e..59be90b6ad 100644 --- a/integrations/openai-agents/python/tests/test_endpoint.py +++ b/integrations/openai-agents/python/tests/test_endpoint.py @@ -18,8 +18,8 @@ from fastapi import FastAPI from fastapi.testclient import TestClient -from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint import ag_ui_openai_agents.agent as agent_module +from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint RUN_INPUT_JSON = { "thread_id": "t1", diff --git a/integrations/openai-agents/python/tests/test_engine_mapping.py b/integrations/openai-agents/python/tests/test_engine_mapping.py index 10bf22f9ea..336c1ad7e0 100644 --- a/integrations/openai-agents/python/tests/test_engine_mapping.py +++ b/integrations/openai-agents/python/tests/test_engine_mapping.py @@ -25,10 +25,10 @@ MCPApprovalRequestItem, ) from agents.items import ToolCallOutputItem -from ag_ui.core import EventType from openai.types.responses import ResponseFunctionToolCall from openai.types.responses.response_output_item import McpApprovalRequest +from ag_ui.core import EventType from ag_ui_openai_agents.engine import SDKToAGUITranslator from ag_ui_openai_agents.engine.stream_types import ( RawResponseEventType, diff --git a/integrations/openai-agents/python/tests/test_multimodal.py b/integrations/openai-agents/python/tests/test_multimodal.py index 6acd19232a..f13ee73170 100644 --- a/integrations/openai-agents/python/tests/test_multimodal.py +++ b/integrations/openai-agents/python/tests/test_multimodal.py @@ -21,7 +21,6 @@ TextInputContent, VideoInputContent, ) - from ag_ui_openai_agents.engine.agui_to_sdk import AGUIToSDKTranslator _engine = AGUIToSDKTranslator() diff --git a/integrations/openai-agents/python/tests/test_snapshot.py b/integrations/openai-agents/python/tests/test_snapshot.py index 5238019d9b..e0df25a63c 100644 --- a/integrations/openai-agents/python/tests/test_snapshot.py +++ b/integrations/openai-agents/python/tests/test_snapshot.py @@ -17,13 +17,6 @@ ToolCallItem, ToolCallOutputItem, ) -from ag_ui.core import ( - AssistantMessage, - EventType, - RunAgentInput, - ToolMessage, - UserMessage, -) from openai.types.responses import ( ResponseFunctionToolCall, ResponseOutputMessage, @@ -31,6 +24,13 @@ ResponseReasoningItem, ) +from ag_ui.core import ( + AssistantMessage, + EventType, + RunAgentInput, + ToolMessage, + UserMessage, +) from ag_ui_openai_agents.engine import SDKToAGUITranslator _AGENT = Agent(name="test-agent") diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index f79630934d..c39b2eb2ce 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -21,6 +21,9 @@ from unittest.mock import MagicMock import pytest +from agents import FunctionTool +from agents.result import RunResultStreaming + from ag_ui.core import ( CustomEvent, EventType, @@ -33,9 +36,6 @@ Tool, UserMessage, ) -from agents import FunctionTool -from agents.result import RunResultStreaming - from ag_ui_openai_agents import AGUITranslator From ac66f1cc03c9b6296514b5c0fb179e3277c39403 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 22:57:10 +0200 Subject: [PATCH 41/94] feat(openai-agents): refactor input translation and streaming logic in README --- integrations/openai-agents/python/README.md | 33 +++++++++++---------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index a472a73917..48cafbb465 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -18,8 +18,8 @@ The flow is: ```python translator = AGUITranslator() -bundle = translator.to_sdk(run_input) -result = Runner.run_streamed(agent, input=bundle.messages) +translated_input = translator.to_sdk(run_input) +result = Runner.run_streamed(agent, input=translated_input.messages) async for event in translator.to_agui(result, run_input): ... @@ -72,24 +72,25 @@ app = FastAPI() @app.post("/") async def run(body: RunAgentInput) -> StreamingResponse: - return StreamingResponse(_stream(body), media_type=encoder.get_content_type()) + """Translate one AG-UI request into an SDK run and stream it back.""" + + async def stream(): + # AGUI input -> OpenAI SDK + translated_input = translator.to_sdk(body) + # merge client-declared tools onto this request's agent + run_agent = agent + if translated_input.tools: + run_agent = agent.clone(tools=[*agent.tools, *translated_input.tools]) -async def _stream(body: RunAgentInput): - # 1. AG-UI input -> SDK-ready bundle. - bundle = translator.to_sdk(body) + # normal OpenAI SDK streaming run + result = Runner.run_streamed(run_agent, input=translated_input.messages) - # 2. Merge client-declared tools per request. - run_agent = agent - if bundle.tools: - run_agent = agent.clone(tools=[*agent.tools, *bundle.tools]) + # OpenAI SDK -> AGUI events + async for event in translator.to_agui(result, body): + yield encoder.encode(event) - # 3. Run the SDK agent normally, then translate the streamed result. - # to_agui wraps it with RUN_STARTED/RUN_FINISHED/RUN_ERROR and - # appends a MESSAGES_SNAPSHOT by default (just before RUN_FINISHED). - result = Runner.run_streamed(run_agent, input=bundle.messages) - async for event in translator.to_agui(result, body): - yield encoder.encode(event) + return StreamingResponse(stream(), media_type=encoder.get_content_type()) ``` Test it: From 0e61b9da3000b6dd0c4e5b5c0b233f7e52b8f18d Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 23:03:42 +0200 Subject: [PATCH 42/94] feat(examples): enhance AG-UI Docs Copilot with rendering tool and progress feedback --- .../feature/(v2)/ag_ui_docs_copilot/page.tsx | 64 +++++++++++++++++++ apps/dojo/src/files.json | 10 +-- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx index 7f52af9864..a52b1cc3c4 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx @@ -5,8 +5,10 @@ import { CopilotKit } from "@copilotkit/react-core"; import { CopilotChat, useConfigureSuggestions, + useRenderTool, } from "@copilotkit/react-core/v2"; import "@copilotkit/react-core/v2/styles.css"; +import { z } from "zod"; interface AGUIDocsCopilotProps { params: Promise<{ integrationId: string }>; @@ -27,6 +29,15 @@ const AGUIDocsCopilot: React.FC = ({ params }) => { }; const DocsChat = () => { + useRenderTool({ + name: "ask_ag_ui_docs", + agentId: "ag_ui_docs_copilot", + parameters: z.object({ input: z.string().optional() }), + render: ({ status, args, result }: any) => ( + + ), + }); + useConfigureSuggestions({ suggestions: [ { @@ -60,4 +71,57 @@ const DocsChat = () => { ); }; +function DocsLookupProgress({ + status, + query, + result, +}: { + status: "inProgress" | "executing" | "complete"; + query?: string; + result?: string; +}) { + const complete = status === "complete"; + const step = complete + ? "Answer ready" + : status === "executing" + ? "Finding the relevant section" + : "Opening the AG-UI guide"; + + return ( +
+
+ + {complete ? "✓" : "📚"} + + + {complete ? "AG-UI docs consulted" : "Consulting the AG-UI docs"} + + {!complete && ( + + + + + + )} +
+
+ {step} +
+ {!complete && query && ( +
+ {query} +
+ )} + {complete && result && ( +
+ Documentation specialist finished. +
+ )} +
+ ); +} + export default AGUIDocsCopilot; diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 2dac76bd7a..116cce1b35 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4816,7 +4816,7 @@ "openai-agents-python::ag_ui_docs_copilot": [ { "name": "page.tsx", - "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_sdk(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default AGUIDocsCopilot;\n", + "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"ask_ag_ui_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_sdk(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n status,\n query,\n result,\n}: {\n status: \"inProgress\" | \"executing\" | \"complete\";\n query?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const step = complete\n ? \"Answer ready\"\n : status === \"executing\"\n ? \"Finding the relevant section\"\n : \"Opening the AG-UI guide\";\n\n return (\n
\n
\n \n {complete ? \"✓\" : \"📚\"}\n \n \n {complete ? \"AG-UI docs consulted\" : \"Consulting the AG-UI docs\"}\n \n {!complete && (\n \n \n \n \n \n )}\n
\n
\n {step}\n
\n {!complete && query && (\n
\n {query}\n
\n )}\n {complete && result && (\n
\n Documentation specialist finished.\n
\n )}\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", "language": "typescript", "type": "file" }, @@ -4828,7 +4828,7 @@ }, { "name": "ag_ui_docs_copilot.py", - "content": "\"\"\"AG-UI documentation assistant with a code-writing agent as a tool.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom agents import Agent, Runner\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui_openai_agents import AGUITranslator\n\nfrom .constants import DEFAULT_MODEL\n\n\nDOCS = Path(__file__).resolve().parents[2].joinpath(\"README.md\").read_text(encoding=\"utf-8\")\n\n# Documentation specialist: knows ag-ui-openai-agents docs\ncode_agent_instructions = f\"\"\"You are the technical specialist for the AG-UI\nintegration with the OpenAI Agents SDK. The documentation below is your source\nof truth. Help developers understand and implement the integration: the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\n\n{DOCS}\n\n\"\"\"\n\ndocs_agent = Agent(\n name=\"AG-UI Documentation Specialist\",\n model=DEFAULT_MODEL,\n instructions=code_agent_instructions\n)\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call ask_ag_ui_docs before\nanswering. Treat the specialist as the source of truth for integration details.\nUse only the part of its result that answers the user's question. Do not invent\nintegration-specific behavior or claim details the specialist did not provide.\nFor code requests, make sure the specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n docs_agent.as_tool(\n tool_name=\"ask_ag_ui_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n )\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_sdk(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", + "content": "\"\"\"AG-UI documentation assistant with a code-writing agent as a tool.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom agents import Agent, Runner\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\nDOCS = Path(__file__).resolve().parents[2].joinpath(\"README.md\").read_text(encoding=\"utf-8\")\n\n# Documentation specialist: knows ag-ui-openai-agents docs\ncode_agent_instructions = f\"\"\"You are the technical specialist for the AG-UI\nintegration with the OpenAI Agents SDK. The documentation below is your source\nof truth. Help developers understand and implement the integration: the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\n\n{DOCS}\n\n\"\"\"\n\ndocs_agent = Agent(\n name=\"AG-UI Documentation Specialist\",\n model=DEFAULT_MODEL,\n instructions=code_agent_instructions\n)\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call ask_ag_ui_docs before\nanswering. Treat the specialist as the source of truth for integration details.\nUse only the part of its result that answers the user's question. Do not invent\nintegration-specific behavior or claim details the specialist did not provide.\nFor code requests, make sure the specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n docs_agent.as_tool(\n tool_name=\"ask_ag_ui_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n )\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_sdk(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", "language": "python", "type": "file" } @@ -4914,7 +4914,7 @@ }, { "name": "human_in_the_loop_approval.py", - "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, Runner, function_tool\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n async def stream():\n decision = None\n if isinstance(body.forwarded_props, dict):\n decision = body.forwarded_props.get(\"approval\")\n\n pending_state = _pending_approvals.pop(body.thread_id, None)\n if decision and pending_state is not None:\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is not None:\n if decision.get(\"approve\"):\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_sdk(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", + "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n async def stream():\n decision = None\n if isinstance(body.forwarded_props, dict):\n decision = body.forwarded_props.get(\"approval\")\n\n pending_state = _pending_approvals.pop(body.thread_id, None)\n if decision and pending_state is not None:\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is not None:\n if decision.get(\"approve\"):\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_sdk(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", "language": "python", "type": "file" } @@ -4980,7 +4980,7 @@ }, { "name": "custom_lifecycle_events.py", - "content": "\"\"\"Custom lifecycle events — CUSTOM events bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. The wrapper receives them as\n``start_custom_event``/``end_custom_event`` factories and forwards their\nresults to the same-named ``to_agui()`` parameters\nparams, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_event() -> CustomEvent:\n tokens = random.randint(20, 120)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"input_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)},\n )\n\n\ndef build_output_usage_event() -> CustomEvent:\n tokens = random.randint(120, 480)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"output_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)},\n )\n\n\nagent = OpenAIAgentsAgent(\n create_custom_lifecycle_events_agent(),\n name=\"custom_lifecycle_events\",\n start_custom_event=build_input_usage_event,\n end_custom_event=build_output_usage_event,\n)\napp = FastAPI(title=\"Custom lifecycle events AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "content": "\"\"\"Custom lifecycle events — CUSTOM events bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. The wrapper receives them as\n``start_custom_event``/``end_custom_event`` factories and forwards their\nresults to the same-named ``to_agui()`` parameters\nparams, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_event() -> CustomEvent:\n tokens = random.randint(20, 120)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"input_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)},\n )\n\n\ndef build_output_usage_event() -> CustomEvent:\n tokens = random.randint(120, 480)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"output_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)},\n )\n\n\nagent = OpenAIAgentsAgent(\n create_custom_lifecycle_events_agent(),\n name=\"custom_lifecycle_events\",\n start_custom_event=build_input_usage_event,\n end_custom_event=build_output_usage_event,\n)\napp = FastAPI(title=\"Custom lifecycle events AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } @@ -5000,7 +5000,7 @@ }, { "name": "dynamic_system_prompt.py", - "content": "\"\"\"Dynamic system prompt — reply language driven by the AG-UI ``context`` channel.\n\nThe frontend sends a language choice in ``RunAgentInput.context`` and the SDK\nrebuilds its instructions for that request.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, RunContextWrapper\nfrom ag_ui.core import Context\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\nBASE_INSTRUCTIONS = (\n \"You are a helpful, concise assistant. Answer the user's questions directly.\"\n)\n\n# Fallback when the frontend hasn't picked a language yet.\nDEFAULT_LANGUAGE = \"English\"\n\n\ndef _read_language(ctx: RunContextWrapper[list[Context]]) -> str:\n \"\"\"Pull the reply language out of the AG-UI context list.\n\n ``ctx.context`` here IS the raw ``list[Context]`` the client sent —\n each item a ``{description, value}`` pair, nothing wrapping it. We match\n the item whose description mentions \"language\" and use its value.\n \"\"\"\n items = ctx.context or []\n for item in items:\n if \"language\" in (item.description or \"\").lower():\n return item.value or DEFAULT_LANGUAGE\n return DEFAULT_LANGUAGE\n\n\ndef dynamic_instructions(ctx: RunContextWrapper[list[Context]], agent: Agent) -> str:\n \"\"\"Native SDK dynamic-instructions hook: build the prompt fresh each turn,\n baking in whatever language the frontend currently has selected.\"\"\"\n language = _read_language(ctx)\n return (\n f\"{BASE_INSTRUCTIONS}\\n\"\n f\"Always reply in {language}, no matter what language the user writes in. \"\n f\"Every word of your response must be in {language}.\"\n )\n\n\ndef create_dynamic_system_prompt_agent() -> Agent:\n return Agent(\n name=\"multilingual_assistant\",\n model=DEFAULT_MODEL,\n instructions=dynamic_instructions,\n )\n\n\nagent = OpenAIAgentsAgent(create_dynamic_system_prompt_agent(), name=\"dynamic_system_prompt\")\napp = FastAPI(title=\"Dynamic system prompt AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "content": "\"\"\"Dynamic system prompt — reply language driven by the AG-UI ``context`` channel.\n\nThe frontend sends a language choice in ``RunAgentInput.context`` and the SDK\nrebuilds its instructions for that request.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, RunContextWrapper\nfrom fastapi import FastAPI\n\nfrom ag_ui.core import Context\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\nBASE_INSTRUCTIONS = (\n \"You are a helpful, concise assistant. Answer the user's questions directly.\"\n)\n\n# Fallback when the frontend hasn't picked a language yet.\nDEFAULT_LANGUAGE = \"English\"\n\n\ndef _read_language(ctx: RunContextWrapper[list[Context]]) -> str:\n \"\"\"Pull the reply language out of the AG-UI context list.\n\n ``ctx.context`` here IS the raw ``list[Context]`` the client sent —\n each item a ``{description, value}`` pair, nothing wrapping it. We match\n the item whose description mentions \"language\" and use its value.\n \"\"\"\n items = ctx.context or []\n for item in items:\n if \"language\" in (item.description or \"\").lower():\n return item.value or DEFAULT_LANGUAGE\n return DEFAULT_LANGUAGE\n\n\ndef dynamic_instructions(ctx: RunContextWrapper[list[Context]], agent: Agent) -> str:\n \"\"\"Native SDK dynamic-instructions hook: build the prompt fresh each turn,\n baking in whatever language the frontend currently has selected.\"\"\"\n language = _read_language(ctx)\n return (\n f\"{BASE_INSTRUCTIONS}\\n\"\n f\"Always reply in {language}, no matter what language the user writes in. \"\n f\"Every word of your response must be in {language}.\"\n )\n\n\ndef create_dynamic_system_prompt_agent() -> Agent:\n return Agent(\n name=\"multilingual_assistant\",\n model=DEFAULT_MODEL,\n instructions=dynamic_instructions,\n )\n\n\nagent = OpenAIAgentsAgent(create_dynamic_system_prompt_agent(), name=\"dynamic_system_prompt\")\napp = FastAPI(title=\"Dynamic system prompt AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } From 459bef617bb1c069fa513055a37ae8d1df08baf2 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Mon, 13 Jul 2026 23:36:24 +0200 Subject: [PATCH 43/94] feat(openai-agents): introduce specialized agents for AG-UI Protocol and OpenAI Agents documentation --- .../(v2)/ag_ui_docs_copilot/README.mdx | 9 ++- .../feature/(v2)/ag_ui_docs_copilot/page.tsx | 30 +++++-- .../openai-agents/python/examples/README.md | 14 ++-- .../agents_examples/ag_ui_docs_copilot.py | 78 ++++++++++++++----- .../tests/test_ag_ui_docs_copilot_example.py | 18 ++++- 5 files changed, 112 insertions(+), 37 deletions(-) diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx index 8a0402fbd9..1536d62850 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx @@ -5,13 +5,14 @@ the OpenAI Agents SDK. The main **Copilot** handles normal conversation without loading the documentation. For AG-UI documentation or code questions, it calls a second -OpenAI Agents SDK agent, **AG-UI Documentation Specialist**, through -`Agent.as_tool()`. Only the specialist receives the local `README.md`. +OpenAI Agents SDK agents, **AG-UI OpenAI Agents Specialist** and **AG-UI +Protocol Python Specialist**, through +`Agent.as_tool()`. Each specialist receives only its relevant local README. ## What it demonstrates ```text -Copilot → Documentation Specialist (local README) +Copilot → OpenAI Agents / Protocol specialists (local READMEs) ↓ RunAgentInput → to_sdk() → Runner.run_streamed() → to_agui() → SSE ``` @@ -32,6 +33,6 @@ documents should replace this with its own retrieval system. - Ask how `AGUITranslator.to_sdk()` and `to_agui()` connect the frontend and SDK. -- Ask the Documentation Specialist to generate a minimal FastAPI streaming endpoint. +- Ask the OpenAI Agents Specialist to generate a minimal FastAPI streaming endpoint. - Ask which translator options control lifecycle events, snapshots, state, and errors. diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx index a52b1cc3c4..9e6286c1e6 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx @@ -30,11 +30,29 @@ const AGUIDocsCopilot: React.FC = ({ params }) => { const DocsChat = () => { useRenderTool({ - name: "ask_ag_ui_docs", + name: "ask_ag_ui_openai_agents_docs", agentId: "ag_ui_docs_copilot", parameters: z.object({ input: z.string().optional() }), render: ({ status, args, result }: any) => ( - + + ), + }); + useRenderTool({ + name: "ask_ag_ui_protocol_docs", + agentId: "ag_ui_docs_copilot", + parameters: z.object({ input: z.string().optional() }), + render: ({ status, args, result }: any) => ( + ), }); @@ -72,10 +90,12 @@ const DocsChat = () => { }; function DocsLookupProgress({ + source, status, query, result, }: { + source: "AG-UI OpenAI Agents" | "AG-UI Protocol"; status: "inProgress" | "executing" | "complete"; query?: string; result?: string; @@ -85,7 +105,7 @@ function DocsLookupProgress({ ? "Answer ready" : status === "executing" ? "Finding the relevant section" - : "Opening the AG-UI guide"; + : `Opening the ${source} guide`; return (
@@ -94,7 +114,7 @@ function DocsLookupProgress({ {complete ? "✓" : "📚"} - {complete ? "AG-UI docs consulted" : "Consulting the AG-UI docs"} + {complete ? `${source} docs consulted` : `Consulting ${source} docs`} {!complete && ( - Documentation specialist finished. + {source} specialist finished.
)} diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index b801f27116..9ec6544622 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -4,8 +4,9 @@ Runnable demos for `ag_ui_openai_agents`, one mounted FastAPI app per agent. The aggregate server shows both integration styles: - **`ag_ui_docs_copilot`** handles normal conversation with a small main - Copilot and delegates AG-UI documentation and code questions to an - `AG-UI Documentation Specialist` agent as a tool. + Copilot and delegates integration questions to an + `AG-UI OpenAI Agents Specialist` and core protocol questions to an + `AG-UI Protocol Python Specialist` as tools. - The remaining focused feature apps use **`OpenAIAgentsAgent`** and `add_openai_agents_fastapi_endpoint` where their run does not require custom control. @@ -60,10 +61,11 @@ lists every registered agent. Demos map 1:1 onto the AG-UI Dojo feature pages. The main Copilot handles normal conversation without carrying the documentation in its instructions. Documentation and code questions are delegated to an -`AG-UI Documentation Specialist`, which receives the integration's local -`README.md` through the SDK's `Agent.as_tool()` API. Its endpoint keeps the -direct `to_sdk` → `Runner.run_streamed` → `to_agui` flow visible and adds no -retrieval framework, vector database, or network dependency. +`AG-UI OpenAI Agents Specialist` and `AG-UI Protocol Python Specialist`, which +receive the integration and core SDK README files through the SDK's +`Agent.as_tool()` API. Its endpoint keeps the direct `to_sdk` → +`Runner.run_streamed` → `to_agui` flow visible and adds no retrieval framework, +vector database, or network dependency. **Try:** `"Explain how to connect my existing OpenAI Agents SDK agent to AG-UI, then ask the Documentation Specialist for the smallest FastAPI streaming diff --git a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py index f5bd729edf..d1fb41ae7b 100644 --- a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py +++ b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py @@ -1,4 +1,4 @@ -"""AG-UI documentation assistant with a code-writing agent as a tool.""" +"""AG-UI documentation assistant with two focused specialists as tools.""" from __future__ import annotations @@ -13,12 +13,43 @@ from ag_ui_openai_agents import AGUITranslator from .constants import DEFAULT_MODEL -DOCS = Path(__file__).resolve().parents[2].joinpath("README.md").read_text(encoding="utf-8") +# ag-ui-protocol specialist: knows the core Python SDK documentation +AG_UI_PROTOCOL_DOCS = Path(__file__).resolve().parents[5].joinpath( + "sdks/python/README.md" +).read_text(encoding="utf-8") +AG_UI_PROTOCOL_DOCS_INSTRUCTIONS = f"""You are the technical specialist for +AG-UI Protocol's core Python SDK. The documentation below is your source of +truth. Help developers understand the ag_ui.core data models, RunAgentInput, +messages, events, event types, and ag_ui.encoder EventEncoder, including how to +create and stream protocol events. -# Documentation specialist: knows ag-ui-openai-agents docs -code_agent_instructions = f"""You are the technical specialist for the AG-UI -integration with the OpenAI Agents SDK. The documentation below is your source -of truth. Help developers understand and implement the integration: the +Answer only the user's question. Retrieve and explain only the relevant parts +of the documentation; do not add a broad tutorial or unrelated integration +details. Be concise and practical. For code requests, provide only the +smallest complete, production-readable Python snippet needed for the request, +followed by a short explanation. Use documented APIs only. If the +documentation does not establish an answer, say so briefly instead of +guessing. + + +{AG_UI_PROTOCOL_DOCS} + +""" + +ag_ui_protocol_docs_agent = Agent( + name="AG-UI Protocol Python Specialist", + model=DEFAULT_MODEL, + instructions=AG_UI_PROTOCOL_DOCS_INSTRUCTIONS, +) + + +# ag-ui-openai-agents specialist: knows the integration documentation +AG_UI_OPENAI_AGENTS_DOCS = Path(__file__).resolve().parents[2].joinpath( + "README.md" +).read_text(encoding="utf-8") +AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS = f"""You are the technical specialist for +AG-UI OpenAI Agents integration. The documentation below is your source of +truth. Help developers understand and implement the integration: the AGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE endpoints, tools, client tools, context, state, lifecycle events, errors, and testing. @@ -32,16 +63,18 @@ an answer, say so briefly instead of guessing. -{DOCS} +{AG_UI_OPENAI_AGENTS_DOCS} """ -docs_agent = Agent( - name="AG-UI Documentation Specialist", +ag_ui_openai_agents_docs_agent = Agent( + name="AG-UI OpenAI Agents Specialist", model=DEFAULT_MODEL, - instructions=code_agent_instructions + instructions=AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS, ) + + # Main Copilot: handles normal conversation and delegates documentation work. copilot_instructions = """You are the developer-facing Copilot for an AG-UI application. Answer only what the user asks. Be clear, practical, and concise; @@ -50,24 +83,33 @@ For any question or request about AG-UI, the OpenAI Agents SDK, translator APIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle -events, errors, tests, or Python implementation, call ask_ag_ui_docs before -answering. Treat the specialist as the source of truth for integration details. -Use only the part of its result that answers the user's question. Do not invent -integration-specific behavior or claim details the specialist did not provide. -For code requests, make sure the specialist is called.""" +events, errors, tests, or Python implementation, call the relevant specialist +before answering. Use ask_ag_ui_openai_agents_docs for the OpenAI Agents SDK +integration and ask_ag_ui_protocol_docs for ag_ui.core protocol types and +EventEncoder questions. +Use only the part of the specialist's result that answers the user's question. +Do not invent behavior or claim details a specialist did not provide. For code +requests, make sure the relevant specialist is called.""" copilot_agent = Agent( name="AG-UI Docs Copilot", model=DEFAULT_MODEL, instructions=copilot_instructions, tools=[ - docs_agent.as_tool( - tool_name="ask_ag_ui_docs", + ag_ui_protocol_docs_agent.as_tool( + tool_name="ask_ag_ui_protocol_docs", + tool_description=( + "Provide authoritative AG-UI core Python SDK guidance about " + "protocol types, RunAgentInput, events, and EventEncoder." + ), + ), + ag_ui_openai_agents_docs_agent.as_tool( + tool_name="ask_ag_ui_openai_agents_docs", tool_description=( "Provide authoritative AG-UI and OpenAI Agents SDK guidance, " "including documented Python integration snippets." ), - ) + ), ], ) diff --git a/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py b/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py index c1c5fc29f8..9c25c744a4 100644 --- a/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py +++ b/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py @@ -13,16 +13,26 @@ def test_docs_copilot_loads_the_integration_readme() -> None: - assert "AG-UI × OpenAI Agents SDK" in ag_ui_docs_copilot.DOCS - assert "AGUITranslator" in ag_ui_docs_copilot.DOCS + assert "AG-UI × OpenAI Agents SDK" in ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS + assert "AGUITranslator" in ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS + assert "ag-ui-protocol" in ag_ui_docs_copilot.AG_UI_PROTOCOL_DOCS + assert "EventEncoder" in ag_ui_docs_copilot.AG_UI_PROTOCOL_DOCS def test_docs_copilot_has_a_documentation_specialist_tool() -> None: assert ag_ui_docs_copilot.copilot_agent.name == "AG-UI Docs Copilot" assert {tool.name for tool in ag_ui_docs_copilot.copilot_agent.tools} == { - "ask_ag_ui_docs" + "ask_ag_ui_openai_agents_docs", + "ask_ag_ui_protocol_docs", } - assert ag_ui_docs_copilot.docs_agent.name == "AG-UI Documentation Specialist" + assert ( + ag_ui_docs_copilot.ag_ui_openai_agents_docs_agent.name + == "AG-UI OpenAI Agents Specialist" + ) + assert ( + ag_ui_docs_copilot.ag_ui_protocol_docs_agent.name + == "AG-UI Protocol Python Specialist" + ) def test_docs_copilot_keeps_the_direct_translator_flow_visible() -> None: From 0d8cf42df43547645e706cae8451e4af60264b74 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 00:36:02 +0200 Subject: [PATCH 44/94] docs(openai-agents): update OpenAI Agents SDK description for clarity --- docs/integrations.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations.mdx b/docs/integrations.mdx index 6365c66c59..3caa17632d 100644 --- a/docs/integrations.mdx +++ b/docs/integrations.mdx @@ -23,7 +23,7 @@ that demonstrate the protocol's capabilities and versatility. - **[CrewAI](https://crewai.com)** - Streamline workflows across industries with powerful AI agents. - **[AG2](https://ag2.ai)** - The Open-Source AgentOS -- **[OpenAI Agents SDK](https://openai.github.io/openai-agents-python/)** - OpenAI's lightweight Python framework for building multi-agent workflows +- **[OpenAI Agents SDK](https://openai.github.io/openai-agents-python/)** - OpenAI’s Python framework for building agentic applications ## Infrastructure / Deployment From 66358299fc9d4a657d260d36973aa00b3de8aaa7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 00:41:57 +0200 Subject: [PATCH 45/94] feat(openai-agents): enhance README and translator documentation for clarity and accuracy --- integrations/openai-agents/python/README.md | 225 +++++++++++++++--- .../src/ag_ui_openai_agents/translator.py | 207 +++++----------- 2 files changed, 255 insertions(+), 177 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 48cafbb465..a6e3b26319 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -5,8 +5,8 @@ with the [AG-UI Protocol](https://github.com/ag-ui-protocol/ag-ui). Build your agent with the OpenAI SDK as usual, then translate its execution into AG-UI events any AG-UI client (e.g. CopilotKit) can render live. -The integration is a **translator**. You keep using the OpenAI Agents -SDK normally; the translator only maps data at the AG-UI boundary: +The integration is a **translator**. You keep using the OpenAI Agents SDK +normally; this package maps data only at the AG-UI boundary: ``` AG-UI RunAgentInput ──to_sdk()──▶ SDK input items + tools @@ -25,10 +25,10 @@ async for event in translator.to_agui(result, run_input): ... ``` -`to_agui(result)` also accepts `result.stream_events()` if your code already +`to_agui(result, run_input)` also accepts `result.stream_events()` if your code already has the SDK event iterator. The stream is always wrapped with `RUN_STARTED` -(first) and `RUN_FINISHED`/`RUN_ERROR` (last) — thread_id/run_id come from -`run_input`, or pass them explicitly. The event just before `RUN_FINISHED` +(first) and `RUN_FINISHED`/`RUN_ERROR` (last); `thread_id` and `run_id` come +from `run_input`. The event just before `RUN_FINISHED` is a `MESSAGES_SNAPSHOT` by default — see [Messages Snapshot](#messages-snapshot). @@ -105,22 +105,23 @@ curl -N -X POST http://localhost:8000 \ }' ``` -Expected: `RUN_STARTED -> STATE_SNAPSHOT -> TEXT_MESSAGE_START -> -TEXT_MESSAGE_CONTENT (xN) -> TEXT_MESSAGE_END -> STATE_SNAPSHOT -> -MESSAGES_SNAPSHOT -> RUN_FINISHED`. +Expected: `RUN_STARTED -> TEXT_MESSAGE_START -> TEXT_MESSAGE_CONTENT (xN) -> +TEXT_MESSAGE_END -> MESSAGES_SNAPSHOT -> RUN_FINISHED`. Add state sources to +emit the optional `STATE_SNAPSHOT` events. -`initial_state` and `final_state` control the two `STATE_SNAPSHOT` slots. -Omit them to echo `run_input.state` as before, pass `None` to suppress a slot, -or pass a static value, zero-argument function, or zero-argument async -function. The initial source is resolved right after `RUN_STARTED`; the final -source is resolved after successful streaming, just before -`MESSAGES_SNAPSHOT`. This lets hooks and tools update application-owned state: +`initial_state` and `final_state` opt into the two `STATE_SNAPSHOT` slots. +Pass a static value, zero-argument function, or zero-argument async function; +`None` (the default) skips that snapshot. The initial source is resolved right +after `RUN_STARTED`; the final source is resolved after successful streaming, +just before `MESSAGES_SNAPSHOT`. This lets hooks and tools update +application-owned state: ```python state = dict(run_input.state or {}) initial_snapshot = dict(state) +translated_input = translator.to_sdk(run_input) -result = Runner.run_streamed(agent, input=bundle.messages, context=state) +result = Runner.run_streamed(agent, input=translated_input.messages, context=state) async for event in translator.to_agui( result, run_input, @@ -180,7 +181,7 @@ agent = OpenAIAgentsAgent( Need finer control (a custom transport, your own SSE framing, per-run branching)? Use the translator directly — see the section above. -## Public API +## API overview Two layers, pick by how much control you want: @@ -215,6 +216,166 @@ fresh per-run engine it needs. Create one instance and share it. | Custom model settings, tracing, guardrails, handoffs | Pass normal OpenAI Agents SDK args to `Runner.run_streamed` | | Custom AG-UI mapping behavior | Subclass an engine translator and inject it into the public translator | +## Public API reference + +### `AGUITranslator` + +`AGUITranslator` is the primary API. It is stateless and reusable: create one +instance for the application, translate each request with `to_sdk`, run the +SDK normally, then translate the resulting stream with `to_agui`. It does not +own your SDK agent, server routes, authentication, or session storage. + +```python +translator = AGUITranslator() +translated_input = translator.to_sdk(run_input) +result = Runner.run_streamed(agent, input=translated_input.messages) + +async for event in translator.to_agui(result, run_input): + yield encoder.encode(event) +``` + +#### Constructor + +```python +AGUITranslator(*, inbound_cls=AGUIToSDKTranslator, outbound_cls=SDKToAGUITranslator) +``` + +These are advanced extension points for changing one mapping without forking +the public orchestration: + +| Parameter | Default | Meaning | +|---|---|---| +| `inbound_cls` | `AGUIToSDKTranslator` | Class used for AG-UI request → SDK input translation. One instance is reused because it is stateless. | +| `outbound_cls` | `SDKToAGUITranslator` | Class used for SDK stream → AG-UI event translation. A fresh instance is created for every run because it tracks open stream windows. | + +For normal use, pass neither parameter. For a custom mapping, subclass the +relevant engine class; see [Advanced: the engine layer](#advanced-the-engine-layer). + +#### `to_sdk(run_input)` + +```python +translated_input = translator.to_sdk(run_input) +``` + +Accepts one `RunAgentInput` and returns `TranslatedInput`: + +| Field | Use | +|---|---| +| `messages` | Responses API items for `Runner.run_streamed(input=...)`. | +| `tools` | Client-owned `FunctionTool` proxies. Clone the agent and merge these tools for this request. | +| `state`, `context`, `forwarded_props`, `thread_id`, `run_id`, `parent_run_id`, `resume` | Original request data, preserved for your application. | + +`TranslatedInput` does not run an agent and does not put `state` or `context` +into prompts automatically. + +#### `to_agui(events, run_input, ...)` + +```python +async for event in translator.to_agui(result, run_input): + yield encoder.encode(event) +``` + +`events` accepts either the `RunResultStreaming` returned by +`Runner.run_streamed` or its `stream_events()` iterator. `run_input` supplies +the lifecycle IDs and message history for snapshots. + +| Parameter | Default | Meaning | +|---|---|---| +| `start_custom_event` | `None` | A `CustomEvent` sent after `RUN_STARTED`. | +| `initial_state` | `None` | Static, sync, or async source for an initial `STATE_SNAPSHOT`. | +| `final_state` | `None` | Static, sync, or async source for a final `STATE_SNAPSHOT`. | +| `emit_messages_snapshot` | `True` | Add `MESSAGES_SNAPSHOT` before the terminal event. | +| `end_custom_event` | `None` | A `CustomEvent` sent after the snapshots and before `RUN_FINISHED`. | +| `emit_run_error` | `True` | Send `RUN_ERROR` if streaming raises, then re-raise the original exception. | +| `run_error_message` | `None` | Client-safe error text; by default the event uses `str(exception)`. | + +`initial_state` and `final_state` can each be a value, a zero-argument +function, or a zero-argument async function. A supplied `None` skips that +snapshot; an empty `{}` is still a valid snapshot. Custom-event values must be +`CustomEvent` objects. The wrapper supports factories for these events; the +direct translator intentionally accepts the event object for this one run. + +Successful runs follow this order: + +```text +RUN_STARTED +start_custom_event (optional) +STATE_SNAPSHOT (initial, optional) +… streamed step, text, tool, and reasoning events … +close any open stream windows +STATE_SNAPSHOT (final, optional) +MESSAGES_SNAPSHOT (optional, enabled by default) +end_custom_event (optional) +RUN_FINISHED +``` + +If the SDK stream raises, the translator emits `RUN_ERROR` when enabled and +re-raises the original exception. It does not emit final state, a messages +snapshot, `end_custom_event`, or `RUN_FINISHED` on that error path. + +### `OpenAIAgentsAgent` + +`OpenAIAgentsAgent` is the convenience wrapper for a fixed SDK `Agent`. +Internally it performs the same `to_sdk` → `Runner.run_streamed` → `to_agui` +flow shown above. Use it when that standard flow is enough; use +`AGUITranslator` directly when your endpoint needs custom branching, +orchestration, or transport behavior. + +```python +wrapped_agent = OpenAIAgentsAgent( + Agent(name="assistant", instructions="Be concise."), +) +``` + +| Constructor parameter | Default | Meaning | +|---|---|---| +| `agent` | required | SDK `Agent` to run. | +| `name` | `agent.name` | Public name returned by the helper health route. | +| `description` | `""` | Optional application metadata. | +| `translator` | new `AGUITranslator` | Translator instance, including any custom engine classes. | +| `run_config` | `None` | `RunConfig` passed to every `Runner.run_streamed` call. | +| `start_custom_event` | `None` | A `CustomEvent` or zero-argument factory, emitted after `RUN_STARTED`. | +| `initial_state` | `None` | Same state-source forms as `AGUITranslator.to_agui`. | +| `final_state` | `None` | Same state-source forms as `AGUITranslator.to_agui`. | +| `emit_messages_snapshot` | `True` | Forwarded to `to_agui`. | +| `end_custom_event` | `None` | A `CustomEvent` or zero-argument factory, emitted before the terminal event. | +| `emit_run_error` | `True` | Forwarded to `to_agui`. | +| `run_error_message` | `None` | Forwarded to `to_agui`. | + +Call `await` through its async iterator with `run(run_input)`. It yields +`BaseEvent` objects; you encode or transport them yourself unless you use the +FastAPI helper. Client-owned tools are merged onto a clone for that run, so a +tool declared by one request never changes the shared SDK agent or leaks into +another request. + +### `add_openai_agents_fastapi_endpoint` + +`add_openai_agents_fastapi_endpoint` connects an `OpenAIAgentsAgent` to a +FastAPI app. It is the highest-level option: it owns HTTP POST handling, AG-UI +SSE encoding, and a small health endpoint, but not your agent's own behavior. + +```python +app = FastAPI() +add_openai_agents_fastapi_endpoint(app, wrapped_agent, "/assistant") +``` + +| Parameter | Default | Meaning | +|---|---|---| +| `app` | required | FastAPI application to register routes on. | +| `agent` | required | `OpenAIAgentsAgent` to execute for incoming requests. | +| `path` | `"/"` | POST route that accepts `RunAgentInput`. | + +For `path="/assistant"`, the helper registers: + +| Route | Behavior | +|---|---| +| `POST /assistant` | Runs the wrapper and returns encoded AG-UI SSE events. | +| `GET /assistant/health` | Returns `{"status": "ok", "agent": {"name": ...}}`. | + +The helper uses the request `Accept` header when creating `EventEncoder`, so +its response content type matches AG-UI content negotiation. It adds routes +only; it does not start a server or manage sessions. + ### Streaming: Live AG-UI Output AG-UI is an ordered event stream by design, so streaming is the primary mode: @@ -222,8 +383,8 @@ AG-UI is an ordered event stream by design, so streaming is the primary mode: ```python translator = AGUITranslator() -bundle = translator.to_sdk(run_input) -result = Runner.run_streamed(agent, input=bundle.messages) +translated_input = translator.to_sdk(run_input) +result = Runner.run_streamed(agent, input=translated_input.messages) async for event in translator.to_agui(result, run_input): ... # AG-UI BaseEvent, ready to encode @@ -247,7 +408,7 @@ All normal OpenAI Agents SDK run options stay on the SDK call: ```python result = Runner.run_streamed( agent, - input=bundle.messages, + input=translated_input.messages, context=my_context, max_turns=8, run_config=run_config, @@ -263,21 +424,21 @@ async for event in translator.to_agui(result, run_input): | AG-UI field | Lands in | |---|---| -| `messages` | `bundle.messages` — Responses-API input items for `Runner.run_streamed` | -| `tools` | `bundle.tools` — SDK `FunctionTool` proxies for client-declared tools; merge with `agent.clone(tools=[*agent.tools, *bundle.tools])` | -| `state`, `context`, `forwarded_props` | passthrough — the library never injects them anywhere; render them into instructions/messages yourself if your app needs the model to see them | +| `messages` | `translated_input.messages` — Responses-API input items for `Runner.run_streamed` | +| `tools` | `translated_input.tools` — SDK `FunctionTool` proxies for client-declared tools; merge with `agent.clone(tools=[*agent.tools, *translated_input.tools])` | +| `state`, `context`, `forwarded_props` | passthrough — the direct translator never injects them into model input; use them in your application as needed | | `thread_id`, `run_id`, `parent_run_id`, `resume` | passthrough | -The most important field is `bundle.messages`; pass it as `input=` to +The most important field is `translated_input.messages`; pass it as `input=` to `Runner.run_streamed`. -If `bundle.tools` is non-empty, merge those tools into the agent for this +If `translated_input.tools` is non-empty, merge those tools into the agent for this request: ```python run_agent = agent -if bundle.tools: - run_agent = agent.clone(tools=[*agent.tools, *bundle.tools]) +if translated_input.tools: + run_agent = agent.clone(tools=[*agent.tools, *translated_input.tools]) ``` ### Lifecycle Events @@ -367,7 +528,7 @@ The translator translates. Your run loop still owns orchestration: | Concern | Owned by | |---|---| | `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` | `to_agui` (always) | -| `STATE_SNAPSHOT` | Your server, if your app needs it | +| `STATE_SNAPSHOT` | `to_agui` when you provide `initial_state` and/or `final_state` | | `MESSAGES_SNAPSHOT` | `to_agui` (on by default; pass `emit_messages_snapshot=False` to own it yourself) | | Session storage and thread history | Your server | | SSE, WebSocket, HTTP response shape | Your server/framework | @@ -419,7 +580,7 @@ Both directions, source of truth is `engine/agui_to_sdk.py` and | `MCPListToolsItem`, `MCPApprovalResponseItem` | dropped (server-side bookkeeping / echo) | | stream start / end (always, via `to_agui`) | `RUN_STARTED` / `RUN_FINISHED` (or `RUN_ERROR`) | | end of stream, `run_input` given (default) | `MESSAGES_SNAPSHOT` | -| `run_input.state`, if set | `STATE_SNAPSHOT` (echoed once by `to_agui`) | +| `initial_state` / `final_state`, if provided | `STATE_SNAPSHOT` | Unknown SDK event or item types translate to `[]` with a debug log — graceful degradation, never a raise. See `tests/test_engine_mapping.py` for @@ -527,16 +688,16 @@ AG-UI's `context` field belongs in the prompt (example below); the SDK's `context=` slot stays yours for whatever your tools need. ```python -bundle = translator.to_sdk(run_input) +translated_input = translator.to_sdk(run_input) instructions = agent.instructions -if bundle.context: +if translated_input.context: instructions += "\n\nContext:\n" + "\n".join( - f"- {item.description}: {item.value}" for item in bundle.context + f"- {item.description}: {item.value}" for item in translated_input.context ) run_agent = agent.clone(instructions=instructions) -result = Runner.run_streamed(run_agent, input=bundle.messages) +result = Runner.run_streamed(run_agent, input=translated_input.messages) ``` ## Capabilities diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 6148c1e734..6483a0e568 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -1,13 +1,4 @@ -"""Streaming translator — the package's main API. - -AGUITranslator pairs with Runner.run_streamed, exposing -to_sdk(run_input) and to_agui(events, run_input). Stateless and reusable — -each to_agui call creates the fresh stateful engine that run needs. -to_agui always wraps the run with RUN_STARTED / RUN_FINISHED / RUN_ERROR — -not optional, every caller needs them, and thread_id/run_id come straight -off run_input. MESSAGES_SNAPSHOT is appended too, by default (see to_agui). -Session persistence is still the caller's job. -""" +"""Translate between AG-UI requests and OpenAI Agents SDK streams.""" from __future__ import annotations @@ -34,14 +25,17 @@ class AGUITranslator: - """Main translator — pairs with Runner.run_streamed. + """Connect an existing streamed OpenAI Agents SDK run to AG-UI. Example: translator = AGUITranslator() - bundle = translator.to_sdk(run_input) - result = Runner.run_streamed(agent, input=bundle.messages, ...) + translated_input = translator.to_sdk(run_input) + result = Runner.run_streamed(agent, input=translated_input.messages) async for event in translator.to_agui(result, run_input): - ... # AG-UI BaseEvent, ready to encode + ... # encode or send the AG-UI event + + The translator does not own the SDK agent, server, or session storage. + See the README for the complete API reference and integration patterns. """ def __init__( @@ -50,28 +44,32 @@ def __init__( inbound_cls: type[AGUIToSDKTranslator] = AGUIToSDKTranslator, outbound_cls: type[SDKToAGUITranslator] = SDKToAGUITranslator, ) -> None: + """Create a translator; engine classes are advanced mapping override points. + + The inbound translator is reused because request translation is stateless. + A new outbound translator is created per run because it tracks that + stream's open text, tool, reasoning, and step windows. + + Args: + inbound_cls: Request-to-SDK translator class. + outbound_cls: SDK-stream-to-AG-UI translator class, created per run. + """ self._inbound = inbound_cls() self._outbound_cls = outbound_cls def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: - """Translate an AG-UI RunAgentInput into an SDK-ready bundle. + """Translate an AG-UI request into SDK input and passthrough data. - The bundle's tools field holds agents.FunctionTool proxies for the - client-declared tools — merge them with the agent's static tools - (agent.clone(tools=[*agent.tools, *bundle.tools])). + The inbound translator performs the complete request conversion, + including client-owned tool proxies. Args: run_input: The incoming AG-UI RunAgentInput. Returns: - TranslatedInput with items, tools, and passthrough state. + SDK-ready messages, client-tool proxies, and request passthrough fields. """ - bundle = self._inbound.translate(run_input) - if run_input.tools: - bundle = bundle.model_copy( - update={"tools": self._inbound.translate_tools(run_input.tools)} - ) - return bundle + return self._inbound.translate(run_input) async def to_agui( self, @@ -86,124 +84,47 @@ async def to_agui( emit_run_error: bool = True, run_error_message: str | None = None, ) -> AsyncIterator[BaseEvent]: - """Translate an SDK event stream into a live AG-UI event stream. + """Translate an SDK event stream into ordered AG-UI events. - Canonical event order (the lifecycle events this method controls - directly; streamed message/tool/reasoning/step events flow through - outbound.translate between #3 and #4): + The translator supplies the lifecycle and snapshot events; the outbound + engine translates streamed text, tool, reasoning, and step events. - 1. RUN_STARTED — always first - 2. start_custom_event — optional, if provided - 3. STATE_SNAPSHOT (initial) — resolve initial_state + 1. RUN_STARTED — always first + 2. start_custom_event (optional) — if provided + 3. STATE_SNAPSHOT (initial, optional) … streamed STEP/TEXT/TOOL/REASONING events … - 4. STEP_FINISHED — final window close (outbound.finalize) - 5. STATE_SNAPSHOT (final) — resolve final_state - 6. MESSAGES_SNAPSHOT — emit_messages_snapshot - 7. end_custom_event — optional, if provided - 8. RUN_FINISHED — always last (or RUN_ERROR on raise) - - Feed the result from Runner.run_streamed, or result.stream_events(). - A fresh stateful engine handles this run's windows; when the stream - ends the engine flush runs automatically — any still-open text / - tool-call / reasoning window is closed before the iterator finishes. + 4. Finalize open stream windows + 5. STATE_SNAPSHOT (final, optional) + 6. MESSAGES_SNAPSHOT (optional) + 7. end_custom_event (optional) + 8. RUN_FINISHED — always last; RUN_ERROR on failure - The first event yielded is always RUN_STARTED and the last is - always RUN_FINISHED (or RUN_ERROR, if the stream raises — the - error event is yielded and then the exception re-raised; this - includes asyncio.CancelledError from a mid-stream timeout or - dropped connection, not just ordinary exceptions). RUN_STARTED / - RUN_FINISHED are not optional; thread_id/run_id come straight off - run_input. - - On error, the RUN_ERROR event carries str(exc) by default — pass - run_error_message to send a fixed string instead (e.g. a generic - "Agent run failed" so raw exception text never reaches the client). - The exception is re-raised regardless, so the caller's own logging - still sees the real one. Pass emit_run_error=False only if you - signal the terminal error yourself in an outer handler — otherwise - a raise with no RUN_ERROR leaves the client watching the stream - just stop. - - Just before RUN_FINISHED, a MESSAGES_SNAPSHOT is appended by - default: run_input.messages, untouched, plus this run's messages — - collected by the engine as it streamed (see - SDKToAGUITranslator.build_messages_snapshot), each under the same - id its streamed event used, so the two can never diverge. Pass - emit_messages_snapshot=False to opt out (e.g. you assemble your - own). Works the same whether events is the RunResultStreaming - object or a bare stream_events() iterator — the snapshot is built - from engine state, not result.new_items. - - State sources may be static values, zero-argument functions, or - zero-argument async functions. initial_state is resolved right after - RUN_STARTED; final_state is resolved near the end, after the SDK - stream and engine flush. Omit either argument to use run_input.state, - or pass None to suppress that snapshot. Empty {} still emits. This - lets application hooks and tools mutate state during a run while the - translator remains unaware of the state shape. Nothing state-related - is emitted on the error path. - - Note: - run_input.messages passes through to the snapshot as-is. If it - carries messages your client should not see echoed back — a - system prompt sent as history instead of via - agent.instructions, for example — filter them out before - calling to_agui: - - filtered = run_input.model_copy( - update={"messages": [m for m in run_input.messages if m.role != "system"]} - ) - async for event in translator.to_agui(result, filtered): - ... + Errors yield ``RUN_ERROR`` by default, then the original exception is + re-raised. See the README for state, snapshots, and error details. Args: - events: The SDK RunResultStreaming object, or the async - iterator returned by its stream_events() method. - run_input: The RunAgentInput this run started from — supplies - thread_id/run_id for the lifecycle events and, unless - emit_messages_snapshot=False, the snapshot's prior-history - half. - start_custom_event: An optional CustomEvent yielded right after - RUN_STARTED (before the STATE_SNAPSHOT), for callers who - need to signal something to the client before any run - content starts flowing. Must be a CustomEvent instance — - anything else raises TypeError. None (the default) emits - nothing. - initial_state: State source resolved right after RUN_STARTED. - Accepts a static value, a zero-argument function, or a - zero-argument async function. Omitted uses run_input.state; - None suppresses the initial snapshot. - final_state: State source resolved after successful streaming and - finalization, just before MESSAGES_SNAPSHOT. Accepts the same - forms as initial_state. Omitted uses run_input.state; None - suppresses the final snapshot. - emit_messages_snapshot: Whether to append a MESSAGES_SNAPSHOT - just before RUN_FINISHED. Defaults to True. - end_custom_event: An optional CustomEvent yielded right before - RUN_FINISHED, after the MESSAGES_SNAPSHOT. Same type - restriction and default as start_custom_event. - emit_run_error: Whether to yield a RUN_ERROR event when the - stream raises, before re-raising. Defaults to True; set - False only if you emit your own terminal error event. - run_error_message: The RUN_ERROR message. Defaults to None, which - sends str(exc); set a fixed string to keep raw exception - text off the wire. + events: A ``RunResultStreaming`` or its ``stream_events()`` iterator. + run_input: The request supplying lifecycle IDs and message history. + start_custom_event: ``CustomEvent`` emitted after ``RUN_STARTED``. + initial_state: Optional static, sync, or async source for the first snapshot. + final_state: Optional static, sync, or async source for the final snapshot. + emit_messages_snapshot: Append ``MESSAGES_SNAPSHOT`` before finishing. + end_custom_event: ``CustomEvent`` emitted before ``RUN_FINISHED``. + emit_run_error: Emit ``RUN_ERROR`` before re-raising a stream error. + run_error_message: Safe fixed ``RUN_ERROR`` message; defaults to ``str(exc)``. Yields: - AG-UI BaseEvent instances, ready to encode. + AG-UI events ready to encode or send. Raises: TypeError: start_custom_event or end_custom_event was given but is not a CustomEvent instance. """ + # validate custom events if start_custom_event and not isinstance(start_custom_event, CustomEvent): - raise TypeError( - f"start_custom_event must be a CustomEvent, got {type(start_custom_event).__name__}" - ) + raise TypeError(f"start_custom_event must be a CustomEvent, got {type(start_custom_event).__name__}") if end_custom_event and not isinstance(end_custom_event, CustomEvent): - raise TypeError( - f"end_custom_event must be a CustomEvent, got {type(end_custom_event).__name__}" - ) + raise TypeError(f"end_custom_event must be a CustomEvent, got {type(end_custom_event).__name__}") # 1. RUN_STARTED — always first yield RunStartedEvent( @@ -212,7 +133,7 @@ async def to_agui( run_id=run_input.run_id, ) - # 2. start_custom_event — optional + # 2. start_custom_event (optional) if start_custom_event: yield start_custom_event @@ -225,6 +146,7 @@ async def to_agui( snapshot=snapshot, ) + # Accept either the SDK result or an iterator already obtained from it. stream_events = ( events.stream_events() if isinstance(events, RunResultStreaming) @@ -232,14 +154,14 @@ async def to_agui( ) outbound = self._outbound_cls() try: - # … streamed STEP / TEXT / TOOL / REASONING events … + # Streamed STEP / TEXT / TOOL / REASONING events. async for sdk_event in stream_events: for event in outbound.translate(sdk_event): yield event - # 4. STEP_FINISHED — final window close + # 4. Close any text, tool, reasoning, or step window still open. for event in outbound.finalize(): yield event - # 5. STATE_SNAPSHOT (final) — resolve now ({} emits, None skips) + # 5. STATE_SNAPSHOT (final, optional) if final_state is not None: snapshot = await self._resolve_state_snapshot(final_state) if snapshot is not None: @@ -247,21 +169,16 @@ async def to_agui( type=EventType.STATE_SNAPSHOT, snapshot=snapshot, ) - # 6. MESSAGES_SNAPSHOT + # 6. MESSAGES_SNAPSHOT (optional) if emit_messages_snapshot: yield outbound.build_messages_snapshot(run_input) except ClientToolPending: - # Not a failure — a client-declared tool's proxy raises this the - # instant the model calls it, specifically so the SDK stops here - # and hands the call back to the client. The TOOL_CALL_START/ - # ARGS/END trio for it was already yielded by the normal - # translate() dispatch above (the run-item event that names the - # call arrives before the SDK ever invokes it), so this is a - # clean end of turn: finalize any open windows, snapshot, finish. - # 4. STEP_FINISHED — final window close + # A client-owned tool ends this server run cleanly; its call events + # have already streamed to the frontend. Finish with steps 4–6. + # 4. Close any text, tool, reasoning, or step window still open. for event in outbound.finalize(): yield event - # 5. STATE_SNAPSHOT (final) — resolve now ({} emits, None skips) + # 5. STATE_SNAPSHOT (final, optional) if final_state is not None: snapshot = await self._resolve_state_snapshot(final_state) if snapshot is not None: @@ -269,7 +186,7 @@ async def to_agui( type=EventType.STATE_SNAPSHOT, snapshot=snapshot, ) - # 6. MESSAGES_SNAPSHOT + # 6. MESSAGES_SNAPSHOT (optional) if emit_messages_snapshot: yield outbound.build_messages_snapshot(run_input) except (Exception, asyncio.CancelledError) as exc: @@ -284,7 +201,7 @@ async def to_agui( ) raise - # 7. end_custom_event — optional + # 7. end_custom_event (optional) if end_custom_event: yield end_custom_event @@ -300,4 +217,4 @@ async def _resolve_state_snapshot(self, state: Any) -> Any: value = state() if callable(state) else state if inspect.isawaitable(value): value = await value - return value \ No newline at end of file + return value From 4b1b0071f4877feaab7eb5abffdfacd68a26f34d Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 00:42:30 +0200 Subject: [PATCH 46/94] feat(openai-agents): rename run method to run_streamed for clarity and update related references --- .../openai-agents/python/src/ag_ui_openai_agents/agent.py | 8 ++++---- .../python/src/ag_ui_openai_agents/endpoint.py | 2 +- .../python/src/ag_ui_openai_agents/engine/agui_to_sdk.py | 8 ++++---- integrations/openai-agents/python/tests/test_agent.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index 6ed083627f..d91c43da9a 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -1,6 +1,6 @@ """Agent wrapper — an OpenAI Agents SDK Agent that speaks AG-UI. -Wrap a plain SDK Agent, get one run(RunAgentInput) yielding AG-UI events, +Wrap a plain SDK Agent, get one run_streamed(RunAgentInput) yielding AG-UI events, ready for add_openai_agents_fastapi_endpoint. """ @@ -31,7 +31,7 @@ class OpenAIAgentsAgent: sdk_agent = Agent(name="assistant", instructions="You are helpful.") agent = OpenAIAgentsAgent(sdk_agent) - async for event in agent.run(run_input): + async for event in agent.run_streamed(run_input): ... """ @@ -51,7 +51,7 @@ def __init__( emit_run_error: bool = True, run_error_message: str | None = None, ) -> None: - """Wrap an SDK Agent. + """Wrap OpenAI Agents SDK Agent. Args: agent: The OpenAI Agents SDK Agent to serve. @@ -89,7 +89,7 @@ def __init__( self._emit_run_error = emit_run_error self._run_error_message = run_error_message - async def run(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: + async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: """Run the agent for one AG-UI request and yield AG-UI events. Orchestration only — the translator does the mapping. to_sdk turns the diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py index c286009226..4d3a22a652 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py @@ -45,7 +45,7 @@ async def openai_agents_endpoint(input_data: RunAgentInput, request: Request): encoder = EventEncoder(accept=accept_header) async def event_generator(): - async for event in agent.run(input_data): + async for event in agent.run_streamed(input_data): yield encoder.encode(event) return StreamingResponse( diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py index 994c1b4f53..64967bc81b 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py @@ -84,10 +84,10 @@ class AGUIToSDKTranslator: Example: One-shot: - bundle = AGUIToSDKTranslator().translate(run_input) + translated_input = AGUIToSDKTranslator().translate(run_input) result = Runner.run_streamed( - agent.clone(tools=agent.tools + bundle.function_tools), - input=bundle.input_items, + agent.clone(tools=agent.tools + translated_input.tools), + input=translated_input.messages, ) Per-item: @@ -131,7 +131,7 @@ def translate(self, run_input: RunAgentInput) -> TranslatedInput: run_id=run_input.run_id, parent_run_id=getattr(run_input, "parent_run_id", None), messages=self.translate_messages(run_input.messages or []), - # tools=self.translate_tools(run_input.tools or []), + tools=self.translate_tools(run_input.tools or []), state=run_input.state, context=list(run_input.context or []), forwarded_props=run_input.forwarded_props, diff --git a/integrations/openai-agents/python/tests/test_agent.py b/integrations/openai-agents/python/tests/test_agent.py index beb7b7f11f..023c464c6d 100644 --- a/integrations/openai-agents/python/tests/test_agent.py +++ b/integrations/openai-agents/python/tests/test_agent.py @@ -78,7 +78,7 @@ def fake_run_streamed(agent, *, input, run_config=None, **kwargs): def _collect(wrapper: OpenAIAgentsAgent, run_input: RunAgentInput) -> list: async def go(): - return [event async for event in wrapper.run(run_input)] + return [event async for event in wrapper.run_streamed(run_input)] return asyncio.run(go()) From 85c0aadc1883a79e4247a6841766a2f1feee246e Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 00:52:56 +0200 Subject: [PATCH 47/94] feat(openai-agents): rename to_sdk() method to to_openai() for consistency and update references --- .../(v2)/ag_ui_docs_copilot/README.mdx | 4 +- .../feature/(v2)/ag_ui_docs_copilot/page.tsx | 2 +- apps/dojo/src/files.json | 8 +- integrations/openai-agents/python/README.md | 84 +++++++++---------- .../openai-agents/python/examples/README.md | 2 +- .../agents_examples/ag_ui_docs_copilot.py | 2 +- .../human_in_the_loop_approval.py | 2 +- .../python/examples/translator_server.py | 6 +- .../python/src/ag_ui_openai_agents/agent.py | 4 +- .../src/ag_ui_openai_agents/translator.py | 6 +- .../tests/test_ag_ui_docs_copilot_example.py | 2 +- .../python/tests/test_translators.py | 12 +-- 12 files changed, 67 insertions(+), 67 deletions(-) diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx index 1536d62850..2e026bf346 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx @@ -14,7 +14,7 @@ Protocol Python Specialist**, through ```text Copilot → OpenAI Agents / Protocol specialists (local READMEs) ↓ -RunAgentInput → to_sdk() → Runner.run_streamed() → to_agui() → SSE +RunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE ``` - Local documentation with no internet request or retrieval framework @@ -31,7 +31,7 @@ documents should replace this with its own retrieval system. ## Try it -- Ask how `AGUITranslator.to_sdk()` and `to_agui()` connect the frontend and +- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and SDK. - Ask the OpenAI Agents Specialist to generate a minimal FastAPI streaming endpoint. - Ask which translator options control lifecycle events, snapshots, state, and diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx index 9e6286c1e6..b21fc03bea 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx @@ -61,7 +61,7 @@ const DocsChat = () => { { title: "Stream an existing agent", message: - "Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_sdk(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.", + "Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.", }, { title: "Map SDK events to AG-UI", diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 116cce1b35..8079c97715 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4816,19 +4816,19 @@ "openai-agents-python::ag_ui_docs_copilot": [ { "name": "page.tsx", - "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"ask_ag_ui_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_sdk(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n status,\n query,\n result,\n}: {\n status: \"inProgress\" | \"executing\" | \"complete\";\n query?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const step = complete\n ? \"Answer ready\"\n : status === \"executing\"\n ? \"Finding the relevant section\"\n : \"Opening the AG-UI guide\";\n\n return (\n
\n
\n \n {complete ? \"✓\" : \"📚\"}\n \n \n {complete ? \"AG-UI docs consulted\" : \"Consulting the AG-UI docs\"}\n \n {!complete && (\n \n \n \n \n \n )}\n
\n
\n {step}\n
\n {!complete && query && (\n
\n {query}\n
\n )}\n {complete && result && (\n
\n Documentation specialist finished.\n
\n )}\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", + "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"ask_ag_ui_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n status,\n query,\n result,\n}: {\n status: \"inProgress\" | \"executing\" | \"complete\";\n query?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const step = complete\n ? \"Answer ready\"\n : status === \"executing\"\n ? \"Finding the relevant section\"\n : \"Opening the AG-UI guide\";\n\n return (\n
\n
\n \n {complete ? \"✓\" : \"📚\"}\n \n \n {complete ? \"AG-UI docs consulted\" : \"Consulting the AG-UI docs\"}\n \n {!complete && (\n \n \n \n \n \n )}\n
\n
\n {step}\n
\n {!complete && query && (\n
\n {query}\n
\n )}\n {complete && result && (\n
\n Documentation specialist finished.\n
\n )}\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nThe main **Copilot** handles normal conversation without loading the\ndocumentation. For AG-UI documentation or code questions, it calls a second\nOpenAI Agents SDK agent, **AG-UI Documentation Specialist**, through\n`Agent.as_tool()`. Only the specialist receives the local `README.md`.\n\n## What it demonstrates\n\n```text\nCopilot → Documentation Specialist (local README)\n ↓\nRunAgentInput → to_sdk() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- An OpenAI Agents SDK documentation specialist used as a tool by Copilot\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded directly\n\nThe integration README is small enough for this focused demo, so it is read\nonce and included in the agents' instructions. This keeps the example\ndeterministic and easy to copy. A production assistant with many or large\ndocuments should replace this with its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_sdk()` and `to_agui()` connect the frontend and\n SDK.\n- Ask the Documentation Specialist to generate a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", + "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nThe main **Copilot** handles normal conversation without loading the\ndocumentation. For AG-UI documentation or code questions, it calls a second\nOpenAI Agents SDK agent, **AG-UI Documentation Specialist**, through\n`Agent.as_tool()`. Only the specialist receives the local `README.md`.\n\n## What it demonstrates\n\n```text\nCopilot → Documentation Specialist (local README)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- An OpenAI Agents SDK documentation specialist used as a tool by Copilot\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded directly\n\nThe integration README is small enough for this focused demo, so it is read\nonce and included in the agents' instructions. This keeps the example\ndeterministic and easy to copy. A production assistant with many or large\ndocuments should replace this with its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask the Documentation Specialist to generate a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", "language": "markdown", "type": "file" }, { "name": "ag_ui_docs_copilot.py", - "content": "\"\"\"AG-UI documentation assistant with a code-writing agent as a tool.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom agents import Agent, Runner\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\nDOCS = Path(__file__).resolve().parents[2].joinpath(\"README.md\").read_text(encoding=\"utf-8\")\n\n# Documentation specialist: knows ag-ui-openai-agents docs\ncode_agent_instructions = f\"\"\"You are the technical specialist for the AG-UI\nintegration with the OpenAI Agents SDK. The documentation below is your source\nof truth. Help developers understand and implement the integration: the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\n\n{DOCS}\n\n\"\"\"\n\ndocs_agent = Agent(\n name=\"AG-UI Documentation Specialist\",\n model=DEFAULT_MODEL,\n instructions=code_agent_instructions\n)\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call ask_ag_ui_docs before\nanswering. Treat the specialist as the source of truth for integration details.\nUse only the part of its result that answers the user's question. Do not invent\nintegration-specific behavior or claim details the specialist did not provide.\nFor code requests, make sure the specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n docs_agent.as_tool(\n tool_name=\"ask_ag_ui_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n )\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_sdk(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", + "content": "\"\"\"AG-UI documentation assistant with a code-writing agent as a tool.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom agents import Agent, Runner\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\nDOCS = Path(__file__).resolve().parents[2].joinpath(\"README.md\").read_text(encoding=\"utf-8\")\n\n# Documentation specialist: knows ag-ui-openai-agents docs\ncode_agent_instructions = f\"\"\"You are the technical specialist for the AG-UI\nintegration with the OpenAI Agents SDK. The documentation below is your source\nof truth. Help developers understand and implement the integration: the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\n\n{DOCS}\n\n\"\"\"\n\ndocs_agent = Agent(\n name=\"AG-UI Documentation Specialist\",\n model=DEFAULT_MODEL,\n instructions=code_agent_instructions\n)\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call ask_ag_ui_docs before\nanswering. Treat the specialist as the source of truth for integration details.\nUse only the part of its result that answers the user's question. Do not invent\nintegration-specific behavior or claim details the specialist did not provide.\nFor code requests, make sure the specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n docs_agent.as_tool(\n tool_name=\"ask_ag_ui_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n )\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", "language": "python", "type": "file" } @@ -4914,7 +4914,7 @@ }, { "name": "human_in_the_loop_approval.py", - "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n async def stream():\n decision = None\n if isinstance(body.forwarded_props, dict):\n decision = body.forwarded_props.get(\"approval\")\n\n pending_state = _pending_approvals.pop(body.thread_id, None)\n if decision and pending_state is not None:\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is not None:\n if decision.get(\"approve\"):\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_sdk(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", + "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n async def stream():\n decision = None\n if isinstance(body.forwarded_props, dict):\n decision = body.forwarded_props.get(\"approval\")\n\n pending_state = _pending_approvals.pop(body.thread_id, None)\n if decision and pending_state is not None:\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is not None:\n if decision.get(\"approve\"):\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_openai(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", "language": "python", "type": "file" } diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index a6e3b26319..23770c8a00 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -9,7 +9,7 @@ The integration is a **translator**. You keep using the OpenAI Agents SDK normally; this package maps data only at the AG-UI boundary: ``` -AG-UI RunAgentInput ──to_sdk()──▶ SDK input items + tools +AG-UI RunAgentInput ──to_openai()──▶ SDK input items + tools SDK streamed result ──to_agui()─▶ AG-UI BaseEvents ``` @@ -18,11 +18,11 @@ The flow is: ```python translator = AGUITranslator() -translated_input = translator.to_sdk(run_input) +translated_input = translator.to_openai(run_input) result = Runner.run_streamed(agent, input=translated_input.messages) async for event in translator.to_agui(result, run_input): - ... + ... ``` `to_agui(result, run_input)` also accepts `result.stream_events()` if your code already @@ -65,32 +65,32 @@ from fastapi.responses import StreamingResponse from ag_ui_openai_agents import AGUITranslator agent = Agent(name="assistant", instructions="Be concise.") -translator = AGUITranslator() # stateless — one instance serves every request -encoder = EventEncoder() # AG-UI's own SSE encoder +translator = AGUITranslator() # stateless — one instance serves every request +encoder = EventEncoder() # AG-UI's own SSE encoder app = FastAPI() @app.post("/") async def run(body: RunAgentInput) -> StreamingResponse: - """Translate one AG-UI request into an SDK run and stream it back.""" + """Translate one AG-UI request into an SDK run and stream it back.""" - async def stream(): - # AGUI input -> OpenAI SDK - translated_input = translator.to_sdk(body) + async def stream(): + # AGUI input -> OpenAI SDK + translated_input = translator.to_openai(body) - # merge client-declared tools onto this request's agent - run_agent = agent - if translated_input.tools: - run_agent = agent.clone(tools=[*agent.tools, *translated_input.tools]) + # merge client-declared tools onto this request's agent + run_agent = agent + if translated_input.tools: + run_agent = agent.clone(tools=[*agent.tools, *translated_input.tools]) - # normal OpenAI SDK streaming run - result = Runner.run_streamed(run_agent, input=translated_input.messages) + # normal OpenAI SDK streaming run + result = Runner.run_streamed(run_agent, input=translated_input.messages) - # OpenAI SDK -> AGUI events - async for event in translator.to_agui(result, body): - yield encoder.encode(event) + # OpenAI SDK -> AGUI events + async for event in translator.to_agui(result, body): + yield encoder.encode(event) - return StreamingResponse(stream(), media_type=encoder.get_content_type()) + return StreamingResponse(stream(), media_type=encoder.get_content_type()) ``` Test it: @@ -119,16 +119,16 @@ application-owned state: ```python state = dict(run_input.state or {}) initial_snapshot = dict(state) -translated_input = translator.to_sdk(run_input) +translated_input = translator.to_openai(run_input) result = Runner.run_streamed(agent, input=translated_input.messages, context=state) async for event in translator.to_agui( - result, - run_input, - initial_state=initial_snapshot, - final_state=lambda: dict(state), + result, + run_input, + initial_state=initial_snapshot, + final_state=lambda: dict(state), ): - ... + ... ``` A full multi-demo server (chat, backend tools, human-in-the-loop, handoffs, @@ -173,7 +173,7 @@ from agents import RunConfig from agents.extensions.models.litellm_provider import LitellmProvider agent = OpenAIAgentsAgent( - Agent(name="assistant", instructions="Be concise.", model="gemini/gemini-2.5-flash"), + Agent(name="assistant", instructions="Be concise.", model="gpt-5.4-mini"), run_config=RunConfig(model_provider=LitellmProvider()), ) ``` @@ -187,7 +187,7 @@ Two layers, pick by how much control you want: | Name | Kind | Use it for | |---|---|---| -| `AGUITranslator` | translator (recommended) | compose it yourself — `to_sdk` + `to_agui`; full control of the agent and server | +| `AGUITranslator` | translator (recommended) | compose it yourself — `to_openai` + `to_agui`; full control of the agent and server | | `OpenAIAgentsAgent` | wrapper class | serve an agent: `run(RunAgentInput) -> AsyncIterator[BaseEvent]` | | `add_openai_agents_fastapi_endpoint(app, agent, path)` | helper | wire a wrapped agent to FastAPI (SSE + `/health`) | @@ -221,13 +221,13 @@ fresh per-run engine it needs. Create one instance and share it. ### `AGUITranslator` `AGUITranslator` is the primary API. It is stateless and reusable: create one -instance for the application, translate each request with `to_sdk`, run the +instance for the application, translate each request with `to_openai`, run the SDK normally, then translate the resulting stream with `to_agui`. It does not own your SDK agent, server routes, authentication, or session storage. ```python translator = AGUITranslator() -translated_input = translator.to_sdk(run_input) +translated_input = translator.to_openai(run_input) result = Runner.run_streamed(agent, input=translated_input.messages) async for event in translator.to_agui(result, run_input): @@ -251,10 +251,10 @@ the public orchestration: For normal use, pass neither parameter. For a custom mapping, subclass the relevant engine class; see [Advanced: the engine layer](#advanced-the-engine-layer). -#### `to_sdk(run_input)` +#### `to_openai(run_input)` ```python -translated_input = translator.to_sdk(run_input) +translated_input = translator.to_openai(run_input) ``` Accepts one `RunAgentInput` and returns `TranslatedInput`: @@ -316,7 +316,7 @@ snapshot, `end_custom_event`, or `RUN_FINISHED` on that error path. ### `OpenAIAgentsAgent` `OpenAIAgentsAgent` is the convenience wrapper for a fixed SDK `Agent`. -Internally it performs the same `to_sdk` → `Runner.run_streamed` → `to_agui` +Internally it performs the same `to_openai` → `Runner.run_streamed` → `to_agui` flow shown above. Use it when that standard flow is enough; use `AGUITranslator` directly when your endpoint needs custom branching, orchestration, or transport behavior. @@ -383,11 +383,11 @@ AG-UI is an ordered event stream by design, so streaming is the primary mode: ```python translator = AGUITranslator() -translated_input = translator.to_sdk(run_input) +translated_input = translator.to_openai(run_input) result = Runner.run_streamed(agent, input=translated_input.messages) async for event in translator.to_agui(result, run_input): - ... # AG-UI BaseEvent, ready to encode + ... # AG-UI BaseEvent, ready to encode ``` You may also pass the SDK event iterator directly: @@ -418,7 +418,7 @@ async for event in translator.to_agui(result, run_input): ... ``` -### What `to_sdk` gives you +### What `to_openai` gives you `TranslatedInput` mirrors `RunAgentInput` field for field: @@ -688,13 +688,13 @@ AG-UI's `context` field belongs in the prompt (example below); the SDK's `context=` slot stays yours for whatever your tools need. ```python -translated_input = translator.to_sdk(run_input) +translated_input = translator.to_openai(run_input) instructions = agent.instructions if translated_input.context: - instructions += "\n\nContext:\n" + "\n".join( - f"- {item.description}: {item.value}" for item in translated_input.context - ) + instructions += "\n\nContext:\n" + "\n".join( + f"- {item.description}: {item.value}" for item in translated_input.context + ) run_agent = agent.clone(instructions=instructions) result = Runner.run_streamed(run_agent, input=translated_input.messages) @@ -751,11 +751,11 @@ browser owns their execution (rendering UI, waiting on the user), not your server. This is the human-in-the-loop mechanism: the same one most AG-UI integrations use, paired with CopilotKit's `useHumanInTheLoop` on the client. -**1. Merge the client tools onto your agent per request.** `to_sdk` turns +**1. Merge the client tools onto your agent per request.** `to_openai` turns each into an SDK `FunctionTool` proxy: ```python -translated = translator.to_sdk(run_input) +translated = translator.to_openai(run_input) agent = base_agent.clone(tools=[*base_agent.tools, *translated.tools]) ``` @@ -775,7 +775,7 @@ base_agent = Agent( **3. The frontend answers; the agent resumes next request.** The tool call streams to the browser, the user acts, and the result comes back as a `ToolMessage` in the next run's `messages` — ordinary multi-turn history. -`to_sdk` translates that `ToolMessage` into a `function_call_output`, so the +`to_openai` translates that `ToolMessage` into a `function_call_output`, so the agent picks up where it left off. No custom event, no `RunState`, no persistence to wire. diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 9ec6544622..fe25aa46aa 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -63,7 +63,7 @@ The main Copilot handles normal conversation without carrying the documentation in its instructions. Documentation and code questions are delegated to an `AG-UI OpenAI Agents Specialist` and `AG-UI Protocol Python Specialist`, which receive the integration and core SDK README files through the SDK's -`Agent.as_tool()` API. Its endpoint keeps the direct `to_sdk` → +`Agent.as_tool()` API. Its endpoint keeps the direct `to_openai` → `Runner.run_streamed` → `to_agui` flow visible and adds no retrieval framework, vector database, or network dependency. diff --git a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py index d1fb41ae7b..26bbf9c48e 100644 --- a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py +++ b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py @@ -126,7 +126,7 @@ async def run_ag_ui_docs_copilot( async def stream(): # AGUI input -> OpenAI SDK - translated_input = translator.to_sdk(body) + translated_input = translator.to_openai(body) # normal OpenAI SDK streaming run result = Runner.run_streamed( diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py index aa4ed5b4d1..abcec9d4f1 100644 --- a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py @@ -109,7 +109,7 @@ async def stream(): pending_state.reject(item) result = Runner.run_streamed(agent, pending_state) else: - translated = _translator.to_sdk(body) + translated = _translator.to_openai(body) run_agent = agent if translated.tools: run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools]) diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index a3cc45dae0..9e4fd9dbbe 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -1,7 +1,7 @@ """ Multi-agent example server — the translator by hand. Recommended. -Wires the translator directly — to_sdk, Runner.run_streamed, to_agui — so the +Wires the translator directly — to_openai, Runner.run_streamed, to_agui — so the full run loop is visible in one place. AGUITranslator is just an events translator: this file keeps full control of the agent and the server. Same demos and output as server.py, which builds the same thing with the @@ -155,7 +155,7 @@ async def _stream_approval(demo: DemoConfig, body: RunAgentInput): pending_state.reject(item) result = Runner.run_streamed(agent, pending_state) else: - translated = translator.to_sdk(body) + translated = translator.to_openai(body) if translated.tools: agent = agent.clone(tools=[*agent.tools, *translated.tools]) result = Runner.run_streamed(agent, input=translated.messages) @@ -216,7 +216,7 @@ async def _stream(demo: DemoConfig, body: RunAgentInput): Each yielded chunk is one SSE line: ``data: \\n\\n`` """ # 1 — Translate AG-UI input into SDK-ready shapes. - translated = translator.to_sdk(body) + translated = translator.to_openai(body) # Frontend (client-owned) tools declared on this request — e.g. the # human_in_the_loop demo's `generate_task_steps`. Merged per-request diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index d91c43da9a..eee6a74442 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -92,7 +92,7 @@ def __init__( async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: """Run the agent for one AG-UI request and yield AG-UI events. - Orchestration only — the translator does the mapping. to_sdk turns the + Orchestration only — the translator does the mapping. to_openai turns the request into SDK input items plus FunctionTool proxies for any client-declared tools; those proxies are merged onto a per-request clone so the static agent stays untouched. to_agui wraps the SDK stream @@ -105,7 +105,7 @@ async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: Yields: AG-UI BaseEvent instances, ready to encode. """ - translated = self._translator.to_sdk(input) + translated = self._translator.to_openai(input) agent = self.agent if translated.tools: diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 6483a0e568..50962fe261 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -29,7 +29,7 @@ class AGUITranslator: Example: translator = AGUITranslator() - translated_input = translator.to_sdk(run_input) + translated_input = translator.to_openai(run_input) result = Runner.run_streamed(agent, input=translated_input.messages) async for event in translator.to_agui(result, run_input): ... # encode or send the AG-UI event @@ -57,8 +57,8 @@ def __init__( self._inbound = inbound_cls() self._outbound_cls = outbound_cls - def to_sdk(self, run_input: RunAgentInput) -> TranslatedInput: - """Translate an AG-UI request into SDK input and passthrough data. + def to_openai(self, run_input: RunAgentInput) -> TranslatedInput: + """Translate an AG-UI request into OpenAI Agents SDK input and passthrough data. The inbound translator performs the complete request conversion, including client-owned tool proxies. diff --git a/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py b/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py index 9c25c744a4..2d096f1cb5 100644 --- a/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py +++ b/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py @@ -38,6 +38,6 @@ def test_docs_copilot_has_a_documentation_specialist_tool() -> None: def test_docs_copilot_keeps_the_direct_translator_flow_visible() -> None: assert "/" in {route.path for route in ag_ui_docs_copilot.app.routes} source = inspect.getsource(ag_ui_docs_copilot.run_ag_ui_docs_copilot) - assert "translator.to_sdk(body)" in source + assert "translator.to_openai(body)" in source assert "Runner.run_streamed(" in source assert "translator.to_agui(result, body)" in source diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index c39b2eb2ce..b69d39837d 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -3,7 +3,7 @@ Covers the public translator contract only — engine mappings have their own coverage: -- ``to_sdk`` delegates to the inbound engine and populates ``tools``. +- ``to_openai`` delegates to the inbound engine and populates ``tools``. - ``AGUITranslator.to_agui`` streams engine output live and appends the engine flush, with a fresh engine per call (reusable translator). - ``to_agui`` always wraps the stream with RUN_STARTED / RUN_FINISHED / @@ -91,12 +91,12 @@ async def _fake_stream(*names: str): yield name -# ── to_sdk ─────────────────────────────────────────────────────────────── +# ── to_openai ──────────────────────────────────────────────────────────── -def test_to_sdk_translates_messages_and_tools(): +def test_to_openai_translates_messages_and_tools(): translator = AGUITranslator() - bundle = translator.to_sdk(_run_input(with_tool=True)) + bundle = translator.to_openai(_run_input(with_tool=True)) assert bundle.thread_id == "t1" assert bundle.messages, "user message should be translated into input items" assert len(bundle.tools) == 1 @@ -104,8 +104,8 @@ def test_to_sdk_translates_messages_and_tools(): assert bundle.tools[0].name == "confirm" -def test_to_sdk_without_tools_leaves_bundle_empty(): - bundle = AGUITranslator().to_sdk(_run_input()) +def test_to_openai_without_tools_leaves_bundle_empty(): + bundle = AGUITranslator().to_openai(_run_input()) assert bundle.tools == [] From cd3a64892f639e78f9b237d8a9695a63fdfe06c6 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 01:34:59 +0200 Subject: [PATCH 48/94] feat(openai-agents): refine documentation and comments for clarity and accuracy --- integrations/openai-agents/python/README.md | 9 ++----- .../openai-agents/python/examples/README.md | 9 +++---- .../examples/agents_examples/constants.py | 10 ++++---- .../python/src/ag_ui_openai_agents/agent.py | 4 ++-- .../ag_ui_openai_agents/engine/sdk_to_agui.py | 24 +++++++++---------- .../python/tests/test_engine_mapping.py | 4 ++-- .../python/tests/test_snapshot.py | 2 +- 7 files changed, 25 insertions(+), 37 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 23770c8a00..e12484fb00 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -163,18 +163,13 @@ add_openai_agents_fastapi_endpoint(app, agent, "/") - holds no per-request state — one instance serves every request; the SDK `Agent` is a config template, and client-declared tools are merged onto a per-request `clone()`, so concurrent requests never see each other's tools; -- takes `run_config=...` to route runs through a non-OpenAI provider (e.g. - LiteLLM) or set run-wide model settings; +- takes `run_config=...` to set run-wide model settings; - exposes `run(RunAgentInput) -> AsyncIterator[BaseEvent]` if you want to serve it on a transport other than FastAPI. ```python -from agents import RunConfig -from agents.extensions.models.litellm_provider import LitellmProvider - agent = OpenAIAgentsAgent( Agent(name="assistant", instructions="Be concise.", model="gpt-5.4-mini"), - run_config=RunConfig(model_provider=LitellmProvider()), ) ``` @@ -497,7 +492,7 @@ The snapshot is `run_input.messages` (untouched, keeping the ids the client already renders) plus this run's messages, built inline as the engine streams — each message's id is resolved once and handed to both the streamed event and the snapshot entry, so they can never disagree, even on -backends that don't stamp real ids (LiteLLM and similar). Reasoning items +backends that don't stamp real ids. Reasoning items are not included; the client keeps its streamed reasoning bubbles through the merge on its own. If the run raises, `to_agui` yields `RUN_ERROR` and re-raises before the snapshot line runs — nothing is emitted on the error diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index fe25aa46aa..429f5f4a9f 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -13,12 +13,9 @@ The aggregate server shows both integration styles: - **`translator_server.py`** remains a compact, centralized direct-translator reference for the original focused demos. -Model provider is **native OpenAI** -(`OPENAI_API_KEY`) — the translators are provider-agnostic, but these -examples exercise the plain-OpenAI path deliberately, since that's what most -integrators will run in production. (LiteLLM/Gemini and the -`FAKE_RESPONSES_ID` handling are covered by the drift-guard/unit tests, not -these examples.) +Model provider is **native OpenAI** (`OPENAI_API_KEY`). These examples exercise +the direct OpenAI path deliberately, since that is the reference setup for the +library. ## Running the server diff --git a/integrations/openai-agents/python/examples/agents_examples/constants.py b/integrations/openai-agents/python/examples/agents_examples/constants.py index d61b9f5ea1..5d25430286 100644 --- a/integrations/openai-agents/python/examples/agents_examples/constants.py +++ b/integrations/openai-agents/python/examples/agents_examples/constants.py @@ -1,11 +1,9 @@ """Shared constants for the example agents. -These examples target **native OpenAI** as the model provider — the -translators are provider-agnostic (they key windows by wire id / ``call_id``, -never assume a specific vendor), but the reference examples exercise the -straightforward path: real ids, orderly ``output_item.done`` events, no -``FAKE_RESPONSES_ID`` placeholder juggling. Point ``DEFAULT_MODEL`` at any -Responses-API model your ``OPENAI_API_KEY`` has access to. +These examples target **native OpenAI** as the model provider. The reference +examples exercise the straightforward OpenAI path: real ids and orderly +``output_item.done`` events. Point ``DEFAULT_MODEL`` at any Responses-API model +your ``OPENAI_API_KEY`` has access to. """ from __future__ import annotations diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index eee6a74442..dd6a4b48d2 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -60,8 +60,8 @@ def __init__( translator: An AGUITranslator to reuse. Defaults to a fresh one; pass your own to inject engine subclasses (per-mapping overrides). run_config: Passed straight to Runner.run_streamed on every run — - the place to set a non-OpenAI model provider (e.g. LiteLLM) or - run-wide model settings. None uses the SDK defaults (native OpenAI). + the place to set run-wide model settings. None uses the SDK + defaults (native OpenAI). start_custom_event: A CustomEvent, or a zero-argument factory that builds one per run, emitted after RUN_STARTED. initial_state: Passed to AGUITranslator.to_agui. Accepts the same diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py index f62824e76c..a17c8282cc 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py @@ -194,8 +194,8 @@ def build_messages_snapshot( — collected here as the stream ran (see _record_* below), each under the exact id its streamed event used. Same id, one resolution: a snapshot message can never disagree with its - streamed counterpart, even on backends that stamp placeholder ids - (Chat Completions, LiteLLM) instead of real ones. + streamed counterpart, even when a backend emits placeholder ids + instead of real ones. Prior history comes from run_input.messages, not result.to_input_list(): Responses-API input items carry no id slot @@ -333,11 +333,10 @@ def translate_output_item_added(self, data: Any) -> list[BaseEvent]: if item_type == SDKItemType.MESSAGE: # Defer TEXT_MESSAGE_START until a real delta actually arrives. - # Some providers (LiteLLM's chat-completions adapter, for one) - # emit a message item even on a turn that ends up being a pure - # tool call with no spoken text — opening the window here would - # leave an empty TEXT_MESSAGE_START/END pair wrapping the tool - # call on the wire. Remember the real id so the lazy open still + # Some backends emit a message item even on a turn that ends up + # being a pure tool call with no spoken text — opening the window + # here would leave an empty TEXT_MESSAGE_START/END pair wrapping + # the tool call on the wire. Remember the real id so the lazy open still # uses it. Output has begun, though, so close any open reasoning # now (some backends send reasoning-done late) — that must not # wait for the deferred text. @@ -813,12 +812,11 @@ def translate_mcp_approval_response_item( def _is_real_id(item_id: Any) -> bool: """Check whether an id is usable on the wire (not empty, not a placeholder). - Some model backends stamp every item with the SDK's - FAKE_RESPONSES_ID sentinel — sharing it across AG-UI events - would collide every message of the run. Checked against the - SDK's own constant, so it's a no-op (always real) on native - OpenAI and correct for any other backend without a - provider-specific branch. + Some backends stamp every item with the SDK's FAKE_RESPONSES_ID + sentinel — sharing it across AG-UI events would collide every + message of the run. Checked against the SDK's own constant, so + it is a no-op on native OpenAI and correct for any backend that + does not use placeholder ids. Args: item_id: The item's wire id, or None. diff --git a/integrations/openai-agents/python/tests/test_engine_mapping.py b/integrations/openai-agents/python/tests/test_engine_mapping.py index 336c1ad7e0..800d324bb7 100644 --- a/integrations/openai-agents/python/tests/test_engine_mapping.py +++ b/integrations/openai-agents/python/tests/test_engine_mapping.py @@ -148,8 +148,8 @@ def test_refusal_delta_streams_into_the_text_window(): def test_text_less_message_item_wrapping_a_tool_call_emits_no_window(): - # A provider (e.g. LiteLLM's chat-completions adapter) can announce a - # message item even on a pure tool-call turn that never carries text: an + # Some backends can announce a message item even on a pure tool-call turn + # that never carries text: an # output_item.added(message), the whole tool call, then # output_item.done(message) — with no text delta in between. The message # window must never open, so the tool call stays a clean sibling and no diff --git a/integrations/openai-agents/python/tests/test_snapshot.py b/integrations/openai-agents/python/tests/test_snapshot.py index e0df25a63c..be9a669a5f 100644 --- a/integrations/openai-agents/python/tests/test_snapshot.py +++ b/integrations/openai-agents/python/tests/test_snapshot.py @@ -213,7 +213,7 @@ def test_unknown_item_types_are_skipped_not_raised(): # arrives. translate_message_output_item/translate_tool_call_item then hit # their "skip" branch (nothing left to close) — that branch used to return # [] without ever recording a snapshot message, so the item streamed fully -# but never made it into MESSAGES_SNAPSHOT. Observed on a real LiteLLM run. +# but never made it into MESSAGES_SNAPSHOT. Observed on a real run. def test_text_message_closed_by_raw_event_before_commit_still_snapshots(): From cd36f8c16c4073b8a558367d986b6fbe0e8e2cba Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 01:58:36 +0200 Subject: [PATCH 49/94] refactor(openai-agents): rename translator classes AGUIToSDKTranslator/SDKToAGUITranslator to OpenAI variants --- apps/dojo/src/files.json | 8 +-- integrations/openai-agents/python/README.md | 22 +++--- .../agents_examples/human_in_the_loop.py | 2 +- .../src/ag_ui_openai_agents/__init__.py | 10 +-- .../ag_ui_openai_agents/engine/__init__.py | 16 ++--- .../{agui_to_sdk.py => agui_to_openai.py} | 8 +-- .../{sdk_to_agui.py => openai_to_agui.py} | 28 ++++---- .../engine/stream_types.py | 4 +- .../src/ag_ui_openai_agents/engine/types.py | 2 +- .../src/ag_ui_openai_agents/translator.py | 8 +-- .../python/tests/test_engine_mapping.py | 72 +++++++++---------- .../python/tests/test_multimodal.py | 6 +- .../python/tests/test_snapshot.py | 22 +++--- .../python/tests/test_stream_types_drift.py | 22 +++--- 14 files changed, 115 insertions(+), 115 deletions(-) rename integrations/openai-agents/python/src/ag_ui_openai_agents/engine/{agui_to_sdk.py => agui_to_openai.py} (99%) rename integrations/openai-agents/python/src/ag_ui_openai_agents/engine/{sdk_to_agui.py => openai_to_agui.py} (98%) diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 8079c97715..b8be5f5bbb 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4816,19 +4816,19 @@ "openai-agents-python::ag_ui_docs_copilot": [ { "name": "page.tsx", - "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"ask_ag_ui_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n status,\n query,\n result,\n}: {\n status: \"inProgress\" | \"executing\" | \"complete\";\n query?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const step = complete\n ? \"Answer ready\"\n : status === \"executing\"\n ? \"Finding the relevant section\"\n : \"Opening the AG-UI guide\";\n\n return (\n
\n
\n \n {complete ? \"✓\" : \"📚\"}\n \n \n {complete ? \"AG-UI docs consulted\" : \"Consulting the AG-UI docs\"}\n \n {!complete && (\n \n \n \n \n \n )}\n
\n
\n {step}\n
\n {!complete && query && (\n
\n {query}\n
\n )}\n {complete && result && (\n
\n Documentation specialist finished.\n
\n )}\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", + "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"ask_ag_ui_openai_agents_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n useRenderTool({\n name: \"ask_ag_ui_protocol_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n source,\n status,\n query,\n result,\n}: {\n source: \"AG-UI OpenAI Agents\" | \"AG-UI Protocol\";\n status: \"inProgress\" | \"executing\" | \"complete\";\n query?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const step = complete\n ? \"Answer ready\"\n : status === \"executing\"\n ? \"Finding the relevant section\"\n : `Opening the ${source} guide`;\n\n return (\n
\n
\n \n {complete ? \"✓\" : \"📚\"}\n \n \n {complete ? `${source} docs consulted` : `Consulting ${source} docs`}\n \n {!complete && (\n \n \n \n \n \n )}\n
\n
\n {step}\n
\n {!complete && query && (\n
\n {query}\n
\n )}\n {complete && result && (\n
\n {source} specialist finished.\n
\n )}\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nThe main **Copilot** handles normal conversation without loading the\ndocumentation. For AG-UI documentation or code questions, it calls a second\nOpenAI Agents SDK agent, **AG-UI Documentation Specialist**, through\n`Agent.as_tool()`. Only the specialist receives the local `README.md`.\n\n## What it demonstrates\n\n```text\nCopilot → Documentation Specialist (local README)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- An OpenAI Agents SDK documentation specialist used as a tool by Copilot\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded directly\n\nThe integration README is small enough for this focused demo, so it is read\nonce and included in the agents' instructions. This keeps the example\ndeterministic and easy to copy. A production assistant with many or large\ndocuments should replace this with its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask the Documentation Specialist to generate a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", + "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nThe main **Copilot** handles normal conversation without loading the\ndocumentation. For AG-UI documentation or code questions, it calls a second\nOpenAI Agents SDK agents, **AG-UI OpenAI Agents Specialist** and **AG-UI\nProtocol Python Specialist**, through\n`Agent.as_tool()`. Each specialist receives only its relevant local README.\n\n## What it demonstrates\n\n```text\nCopilot → OpenAI Agents / Protocol specialists (local READMEs)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- An OpenAI Agents SDK documentation specialist used as a tool by Copilot\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded directly\n\nThe integration README is small enough for this focused demo, so it is read\nonce and included in the agents' instructions. This keeps the example\ndeterministic and easy to copy. A production assistant with many or large\ndocuments should replace this with its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask the OpenAI Agents Specialist to generate a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", "language": "markdown", "type": "file" }, { "name": "ag_ui_docs_copilot.py", - "content": "\"\"\"AG-UI documentation assistant with a code-writing agent as a tool.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom agents import Agent, Runner\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\nDOCS = Path(__file__).resolve().parents[2].joinpath(\"README.md\").read_text(encoding=\"utf-8\")\n\n# Documentation specialist: knows ag-ui-openai-agents docs\ncode_agent_instructions = f\"\"\"You are the technical specialist for the AG-UI\nintegration with the OpenAI Agents SDK. The documentation below is your source\nof truth. Help developers understand and implement the integration: the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\n\n{DOCS}\n\n\"\"\"\n\ndocs_agent = Agent(\n name=\"AG-UI Documentation Specialist\",\n model=DEFAULT_MODEL,\n instructions=code_agent_instructions\n)\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call ask_ag_ui_docs before\nanswering. Treat the specialist as the source of truth for integration details.\nUse only the part of its result that answers the user's question. Do not invent\nintegration-specific behavior or claim details the specialist did not provide.\nFor code requests, make sure the specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n docs_agent.as_tool(\n tool_name=\"ask_ag_ui_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n )\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", + "content": "\"\"\"AG-UI documentation assistant with two focused specialists as tools.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom agents import Agent, Runner\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# ag-ui-protocol specialist: knows the core Python SDK documentation\nAG_UI_PROTOCOL_DOCS = Path(__file__).resolve().parents[5].joinpath(\n \"sdks/python/README.md\"\n).read_text(encoding=\"utf-8\")\nAG_UI_PROTOCOL_DOCS_INSTRUCTIONS = f\"\"\"You are the technical specialist for\nAG-UI Protocol's core Python SDK. The documentation below is your source of\ntruth. Help developers understand the ag_ui.core data models, RunAgentInput,\nmessages, events, event types, and ag_ui.encoder EventEncoder, including how to\ncreate and stream protocol events.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial or unrelated integration\ndetails. Be concise and practical. For code requests, provide only the\nsmallest complete, production-readable Python snippet needed for the request,\nfollowed by a short explanation. Use documented APIs only. If the\ndocumentation does not establish an answer, say so briefly instead of\nguessing.\n\n\n{AG_UI_PROTOCOL_DOCS}\n\n\"\"\"\n\nag_ui_protocol_docs_agent = Agent(\n name=\"AG-UI Protocol Python Specialist\",\n model=DEFAULT_MODEL,\n instructions=AG_UI_PROTOCOL_DOCS_INSTRUCTIONS,\n)\n\n\n# ag-ui-openai-agents specialist: knows the integration documentation\nAG_UI_OPENAI_AGENTS_DOCS = Path(__file__).resolve().parents[2].joinpath(\n \"README.md\"\n).read_text(encoding=\"utf-8\")\nAG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS = f\"\"\"You are the technical specialist for\nAG-UI OpenAI Agents integration. The documentation below is your source of\ntruth. Help developers understand and implement the integration: the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\n\n{AG_UI_OPENAI_AGENTS_DOCS}\n\n\"\"\"\n\nag_ui_openai_agents_docs_agent = Agent(\n name=\"AG-UI OpenAI Agents Specialist\",\n model=DEFAULT_MODEL,\n instructions=AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS,\n)\n\n\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call the relevant specialist\nbefore answering. Use ask_ag_ui_openai_agents_docs for the OpenAI Agents SDK\nintegration and ask_ag_ui_protocol_docs for ag_ui.core protocol types and\nEventEncoder questions.\nUse only the part of the specialist's result that answers the user's question.\nDo not invent behavior or claim details a specialist did not provide. For code\nrequests, make sure the relevant specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n ag_ui_protocol_docs_agent.as_tool(\n tool_name=\"ask_ag_ui_protocol_docs\",\n tool_description=(\n \"Provide authoritative AG-UI core Python SDK guidance about \"\n \"protocol types, RunAgentInput, events, and EventEncoder.\"\n ),\n ),\n ag_ui_openai_agents_docs_agent.as_tool(\n tool_name=\"ask_ag_ui_openai_agents_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n ),\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", "language": "python", "type": "file" } @@ -4894,7 +4894,7 @@ }, { "name": "human_in_the_loop.py", - "content": "\"\"\"Human-in-the-loop — a *frontend*-owned tool, approved before execution.\n\nUnlike :mod:`backend_tool_rendering`, ``generate_task_steps`` has no server\nimplementation — it arrives per-request as an AG-UI client tool\n(``RunAgentInput.tools``), gets wrapped into an SDK ``FunctionTool`` proxy by\n``AGUIToSDKTranslator.translate_tools()``, and is merged onto this agent by\nthe run loop (see ``server.py``).\n\n``tool_use_behavior=StopAtTools(...)`` is the SDK's built-in for \"the model\nmay only *call* this tool, never execute it\": the run ends the moment the\nmodel emits the call, before the (dead-code) proxy body would ever run. The\nfrontend renders the steps for user approval, then sends the result back as\nan AG-UI ``ToolMessage`` in the *next* request — ordinary multi-turn history,\nno custom pause/resume machinery needed.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_task_steps\"\n\nINSTRUCTIONS = \"\"\"You are a task planning assistant that breaks work into clear, actionable steps.\n\nWhen the user asks for help with a task:\n1. Immediately call the `generate_task_steps` tool with an array of steps.\n Each step is an object: {\"description\": \"...\", \"status\": \"enabled\"}.\n2. Do not restate the steps as plain text — the frontend renders them.\n3. After the call, wait for the user to approve/select steps; do not call\n the tool again until they respond.\n\"\"\"\n\n\ndef create_human_in_the_loop_agent() -> Agent:\n return Agent(\n name=\"task_planner\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n\n\nagent = OpenAIAgentsAgent(create_human_in_the_loop_agent(), name=\"human_in_the_loop\")\napp = FastAPI(title=\"Human in the loop AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "content": "\"\"\"Human-in-the-loop — a *frontend*-owned tool, approved before execution.\n\nUnlike :mod:`backend_tool_rendering`, ``generate_task_steps`` has no server\nimplementation — it arrives per-request as an AG-UI client tool\n(``RunAgentInput.tools``), gets wrapped into an SDK ``FunctionTool`` proxy by\n``AGUIToOpenAITranslator.translate_tools()``, and is merged onto this agent by\nthe run loop (see ``server.py``).\n\n``tool_use_behavior=StopAtTools(...)`` is the SDK's built-in for \"the model\nmay only *call* this tool, never execute it\": the run ends the moment the\nmodel emits the call, before the (dead-code) proxy body would ever run. The\nfrontend renders the steps for user approval, then sends the result back as\nan AG-UI ``ToolMessage`` in the *next* request — ordinary multi-turn history,\nno custom pause/resume machinery needed.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_task_steps\"\n\nINSTRUCTIONS = \"\"\"You are a task planning assistant that breaks work into clear, actionable steps.\n\nWhen the user asks for help with a task:\n1. Immediately call the `generate_task_steps` tool with an array of steps.\n Each step is an object: {\"description\": \"...\", \"status\": \"enabled\"}.\n2. Do not restate the steps as plain text — the frontend renders them.\n3. After the call, wait for the user to approve/select steps; do not call\n the tool again until they respond.\n\"\"\"\n\n\ndef create_human_in_the_loop_agent() -> Agent:\n return Agent(\n name=\"task_planner\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n\n\nagent = OpenAIAgentsAgent(create_human_in_the_loop_agent(), name=\"human_in_the_loop\")\napp = FastAPI(title=\"Human in the loop AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index e12484fb00..b327967d6a 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -232,7 +232,7 @@ async for event in translator.to_agui(result, run_input): #### Constructor ```python -AGUITranslator(*, inbound_cls=AGUIToSDKTranslator, outbound_cls=SDKToAGUITranslator) +AGUITranslator(*, inbound_cls=AGUIToOpenAITranslator, outbound_cls=OpenAIToAGUITranslator) ``` These are advanced extension points for changing one mapping without forking @@ -240,8 +240,8 @@ the public orchestration: | Parameter | Default | Meaning | |---|---|---| -| `inbound_cls` | `AGUIToSDKTranslator` | Class used for AG-UI request → SDK input translation. One instance is reused because it is stateless. | -| `outbound_cls` | `SDKToAGUITranslator` | Class used for SDK stream → AG-UI event translation. A fresh instance is created for every run because it tracks open stream windows. | +| `inbound_cls` | `AGUIToOpenAITranslator` | Class used for AG-UI request → SDK input translation. One instance is reused because it is stateless. | +| `outbound_cls` | `OpenAIToAGUITranslator` | Class used for SDK stream → AG-UI event translation. A fresh instance is created for every run because it tracks open stream windows. | For normal use, pass neither parameter. For a custom mapping, subclass the relevant engine class; see [Advanced: the engine layer](#advanced-the-engine-layer). @@ -535,10 +535,10 @@ aiohttp, raw ASGI, WebSockets, or tests can all use the same translator calls. ### Event Mapping -Both directions, source of truth is `engine/agui_to_sdk.py` and -`engine/sdk_to_agui.py`. +Both directions, source of truth is `engine/agui_to_openai.py` and +`engine/openai_to_agui.py`. -**Inbound** — AG-UI `Message` → Responses-API input item (`AGUIToSDKTranslator`): +**Inbound** — AG-UI `Message` → Responses-API input item (`AGUIToOpenAITranslator`): | AG-UI type | SDK / Responses-API shape | |---|---| @@ -553,7 +553,7 @@ Both directions, source of truth is `engine/agui_to_sdk.py` and | Content parts: `TextInputContent`, `ImageInputContent`, `AudioInputContent`, `DocumentInputContent`, `BinaryInputContent` | `input_text` / `input_image` / `input_audio` / `input_file` parts | | `VideoInputContent` | dropped (no OpenAI API accepts video input yet) | -**Outbound** — SDK stream event → AG-UI `BaseEvent` (`SDKToAGUITranslator`): +**Outbound** — SDK stream event → AG-UI `BaseEvent` (`OpenAIToAGUITranslator`): | SDK event / item | AG-UI event(s) | |---|---| @@ -718,9 +718,9 @@ Unknown SDK event types are skipped with a debug log instead of crashing the run The public translator delegates to two independent, symmetric engine translators in `ag_ui_openai_agents.engine`: -- `AGUIToSDKTranslator` — inbound; stateless, tiered per-type methods +- `AGUIToOpenAITranslator` — inbound; stateless, tiered per-type methods (`translate_user_message`, `translate_tool_message`, ...) -- `SDKToAGUITranslator` — outbound; stateful per run (open text/tool/reasoning +- `OpenAIToAGUITranslator` — outbound; stateful per run (open text/tool/reasoning windows), per-type methods (`translate_text_delta`, `translate_item`, ...) Every per-type method is a public override point. To customize one mapping, @@ -729,10 +729,10 @@ untouched: ```python from ag_ui_openai_agents import AGUITranslator -from ag_ui_openai_agents.engine import SDKToAGUITranslator +from ag_ui_openai_agents.engine import OpenAIToAGUITranslator -class MyOutbound(SDKToAGUITranslator): +class MyOutbound(OpenAIToAGUITranslator): def translate_text_delta(self, data): ... # your variant of one mapping diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py index 00053b0295..a219316785 100644 --- a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop.py @@ -3,7 +3,7 @@ Unlike :mod:`backend_tool_rendering`, ``generate_task_steps`` has no server implementation — it arrives per-request as an AG-UI client tool (``RunAgentInput.tools``), gets wrapped into an SDK ``FunctionTool`` proxy by -``AGUIToSDKTranslator.translate_tools()``, and is merged onto this agent by +``AGUIToOpenAITranslator.translate_tools()``, and is merged onto this agent by the run loop (see ``server.py``). ``tool_use_behavior=StopAtTools(...)`` is the SDK's built-in for "the model diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py index 82b5d4dfb9..ef13e05214 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py @@ -17,7 +17,7 @@ Advanced (per-mapping overrides) — the engine layer: - from ag_ui_openai_agents.engine import AGUIToSDKTranslator, SDKToAGUITranslator + from ag_ui_openai_agents.engine import AGUIToOpenAITranslator, OpenAIToAGUITranslator """ from __future__ import annotations @@ -25,9 +25,9 @@ from .agent import OpenAIAgentsAgent from .endpoint import add_openai_agents_fastapi_endpoint from .engine import ( - AGUIToSDKTranslator, + AGUIToOpenAITranslator, ClientToolPending, - SDKToAGUITranslator, + OpenAIToAGUITranslator, TranslatedInput, ) from .translator import AGUITranslator @@ -41,8 +41,8 @@ # Public translator (primary API — 2 methods) "AGUITranslator", # Engine translators (advanced / per-mapping overrides) - "AGUIToSDKTranslator", - "SDKToAGUITranslator", + "AGUIToOpenAITranslator", + "OpenAIToAGUITranslator", # Types & helpers "TranslatedInput", "ClientToolPending", diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py index eb40f00c97..e11c6dbda6 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py @@ -7,7 +7,7 @@ from __future__ import annotations -from .agui_to_sdk import AGUIToSDKTranslator, ClientToolPending +from .agui_to_openai import AGUIToOpenAITranslator, ClientToolPending from .helpers import ( coerce_to_str, new_message_id, @@ -15,25 +15,25 @@ new_tool_result_id, read_attr, ) -from .sdk_to_agui import SDKToAGUITranslator +from .openai_to_agui import OpenAIToAGUITranslator from .stream_types import ( HOSTED_TOOL_CALL_TYPES, RawResponseEventType, - SDKItemType, - SDKStreamEventType, + OpenAIItemType, + OpenAIStreamEventType, ) from .types import TranslatedInput __all__ = [ # Engine translators - "AGUIToSDKTranslator", - "SDKToAGUITranslator", + "AGUIToOpenAITranslator", + "OpenAIToAGUITranslator", # Result types "TranslatedInput", # Wire discriminators - "SDKStreamEventType", + "OpenAIStreamEventType", "RawResponseEventType", - "SDKItemType", + "OpenAIItemType", "HOSTED_TOOL_CALL_TYPES", # Sentinels "ClientToolPending", diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py similarity index 99% rename from integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py rename to integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index 64967bc81b..5d5e8fd0ac 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_sdk.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -78,13 +78,13 @@ def __init__(self, tool_name: str, tool_call_id: str, arguments: str) -> None: # The translator # --------------------------------------------------------------------------- -class AGUIToSDKTranslator: +class AGUIToOpenAITranslator: """Translate AG-UI inbound primitives into OpenAI Agents SDK shapes. Example: One-shot: - translated_input = AGUIToSDKTranslator().translate(run_input) + translated_input = AGUIToOpenAITranslator().translate(run_input) result = Runner.run_streamed( agent.clone(tools=agent.tools + translated_input.tools), input=translated_input.messages, @@ -92,7 +92,7 @@ class AGUIToSDKTranslator: Per-item: - translator = AGUIToSDKTranslator() + translator = AGUIToOpenAITranslator() items = [translator.translate_user_message(msg) for msg in user_msgs] proxy = translator.translate_tool(my_tool) image_part = translator.translate_image_content(part) @@ -788,6 +788,6 @@ def _binary_as_file( __all__ = [ - "AGUIToSDKTranslator", + "AGUIToOpenAITranslator", "ClientToolPending", ] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py similarity index 98% rename from integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py rename to integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index a17c8282cc..e07c2fe6b3 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/sdk_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -1,6 +1,6 @@ """Outbound translator: OpenAI Agents SDK events → AG-UI BaseEvent. -Same layered shape as AGUIToSDKTranslator, just the other direction. +Same layered shape as AGUIToOpenAITranslator, just the other direction. Stateful per run — it tracks open text / tool-call / reasoning windows so AG-UI always sees strict START → CONTENT → END triplets. Make a fresh one per run; don't share across runs. @@ -68,14 +68,14 @@ from .stream_types import ( HOSTED_TOOL_CALL_TYPES, RawResponseEventType, - SDKItemType, - SDKStreamEventType, + OpenAIItemType, + OpenAIStreamEventType, ) logger = logging.getLogger(__name__) -class SDKToAGUITranslator: +class OpenAIToAGUITranslator: """Translate OpenAI Agents SDK outputs into AG-UI BaseEvent objects. Wire ids are reused, never invented — with one caveat: some model @@ -146,11 +146,11 @@ def translate(self, sdk_event: Any) -> list[BaseEvent]: Zero or more translated AG-UI events. """ event_type = read_attr(sdk_event, "type") - if event_type == SDKStreamEventType.RAW_RESPONSE: + if event_type == OpenAIStreamEventType.RAW_RESPONSE: return self.translate_raw_response_event(sdk_event) - if event_type == SDKStreamEventType.RUN_ITEM: + if event_type == OpenAIStreamEventType.RUN_ITEM: return self.translate_run_item_event(sdk_event) - if event_type == SDKStreamEventType.AGENT_UPDATED: + if event_type == OpenAIStreamEventType.AGENT_UPDATED: return self.translate_agent_updated_event(sdk_event) logger.debug("Unknown SDK stream event type: %s", event_type) return [] @@ -331,7 +331,7 @@ def translate_output_item_added(self, data: Any) -> list[BaseEvent]: item_id = read_attr(item, "id") key = self._window_key(item_id, read_attr(data, "output_index")) - if item_type == SDKItemType.MESSAGE: + if item_type == OpenAIItemType.MESSAGE: # Defer TEXT_MESSAGE_START until a real delta actually arrives. # Some backends emit a message item even on a turn that ends up # being a pure tool call with no spoken text — opening the window @@ -342,11 +342,11 @@ def translate_output_item_added(self, data: Any) -> list[BaseEvent]: # wait for the deferred text. self._pending_text_ids[key] = self._resolve_id(item_id, new_message_id) return self._close_all_reasonings() - if item_type == SDKItemType.FUNCTION_CALL: + if item_type == OpenAIItemType.FUNCTION_CALL: call_id = read_attr(item, "call_id") or self._resolve_id(item_id, new_tool_call_id) name = read_attr(item, "name") or "" return self._open_tool_call(key, call_id, name) - if item_type == SDKItemType.REASONING: + if item_type == OpenAIItemType.REASONING: return self._open_reasoning(key, self._resolve_id(item_id, new_reasoning_id)) if item_type in HOSTED_TOOL_CALL_TYPES: call_id = self._resolve_id(item_id, new_tool_call_id) @@ -370,16 +370,16 @@ def translate_output_item_done(self, data: Any) -> list[BaseEvent]: item_type = read_attr(item, "type") key = self._window_key(read_attr(item, "id"), read_attr(data, "output_index")) - if item_type == SDKItemType.MESSAGE: + if item_type == OpenAIItemType.MESSAGE: # Drop a deferred-but-never-opened window: no delta ever arrived, # so there's nothing on the wire to close and no empty window to # emit. If a delta did arrive the pending entry is already gone # and this pop is a no-op; _close_text then closes the real one. self._pending_text_ids.pop(key, None) return self._close_text(key) - if item_type == SDKItemType.FUNCTION_CALL or item_type in HOSTED_TOOL_CALL_TYPES: + if item_type == OpenAIItemType.FUNCTION_CALL or item_type in HOSTED_TOOL_CALL_TYPES: return self._close_tool_call(key) - if item_type == SDKItemType.REASONING: + if item_type == OpenAIItemType.REASONING: events = self._emit_encrypted_value(key, item) events.extend(self._close_reasoning(key)) return events @@ -1108,4 +1108,4 @@ def _record_result(self, call_id: str, content: str) -> str: return result_id -__all__ = ["SDKToAGUITranslator"] +__all__ = ["OpenAIToAGUITranslator"] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py index 0a8f6116db..91b636b472 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py @@ -11,7 +11,7 @@ from enum import Enum -class SDKStreamEventType(str, Enum): +class OpenAIStreamEventType(str, Enum): """Top-level StreamEvent.type values yielded by Runner.run_streamed.""" RAW_RESPONSE = "raw_response_event" @@ -39,7 +39,7 @@ class RawResponseEventType(str, Enum): REASONING_TEXT_DONE = "response.reasoning_text.done" -class SDKItemType(str, Enum): +class OpenAIItemType(str, Enum): """Output-item type values carried by output_item.added / .done.""" MESSAGE = "message" diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index ed94688dcc..f2eac03079 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -1,7 +1,7 @@ """Public data containers for the translator package. Holds typed result objects produced by the translators. Translator -behavior lives in agui_to_sdk.py and sdk_to_agui.py; this module only +behavior lives in agui_to_openai.py and openai_to_agui.py; this module only describes the shapes those translators hand back. """ diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 50962fe261..5bddabe764 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -19,8 +19,8 @@ RunStartedEvent, StateSnapshotEvent, ) -from .engine.agui_to_sdk import AGUIToSDKTranslator, ClientToolPending -from .engine.sdk_to_agui import SDKToAGUITranslator +from .engine.agui_to_openai import AGUIToOpenAITranslator, ClientToolPending +from .engine.openai_to_agui import OpenAIToAGUITranslator from .engine.types import TranslatedInput @@ -41,8 +41,8 @@ class AGUITranslator: def __init__( self, *, - inbound_cls: type[AGUIToSDKTranslator] = AGUIToSDKTranslator, - outbound_cls: type[SDKToAGUITranslator] = SDKToAGUITranslator, + inbound_cls: type[AGUIToOpenAITranslator] = AGUIToOpenAITranslator, + outbound_cls: type[OpenAIToAGUITranslator] = OpenAIToAGUITranslator, ) -> None: """Create a translator; engine classes are advanced mapping override points. diff --git a/integrations/openai-agents/python/tests/test_engine_mapping.py b/integrations/openai-agents/python/tests/test_engine_mapping.py index 800d324bb7..8708889928 100644 --- a/integrations/openai-agents/python/tests/test_engine_mapping.py +++ b/integrations/openai-agents/python/tests/test_engine_mapping.py @@ -1,5 +1,5 @@ """ -Tests for SDKToAGUITranslator's streaming event mapping. +Tests for OpenAIToAGUITranslator's streaming event mapping. The rest of the suite pins ids (test_snapshot) and the wire strings (test_stream_types_drift); this one pins the actual translation: feed the @@ -29,11 +29,11 @@ from openai.types.responses.response_output_item import McpApprovalRequest from ag_ui.core import EventType -from ag_ui_openai_agents.engine import SDKToAGUITranslator +from ag_ui_openai_agents.engine import OpenAIToAGUITranslator from ag_ui_openai_agents.engine.stream_types import ( RawResponseEventType, - SDKItemType, - SDKStreamEventType, + OpenAIItemType, + OpenAIStreamEventType, ) _AGENT = Agent(name="test-agent") @@ -45,12 +45,12 @@ def _raw(kind: RawResponseEventType, **data) -> SimpleNamespace: """A RawResponsesStreamEvent wrapping one Responses payload.""" return SimpleNamespace( - type=SDKStreamEventType.RAW_RESPONSE, + type=OpenAIStreamEventType.RAW_RESPONSE, data=SimpleNamespace(type=kind, **data), ) -def _added(item_type: SDKItemType, output_index: int = 0, **item) -> SimpleNamespace: +def _added(item_type: OpenAIItemType, output_index: int = 0, **item) -> SimpleNamespace: return _raw( RawResponseEventType.OUTPUT_ITEM_ADDED, item=SimpleNamespace(type=item_type, **item), @@ -58,7 +58,7 @@ def _added(item_type: SDKItemType, output_index: int = 0, **item) -> SimpleNames ) -def _done(item_type: SDKItemType, output_index: int = 0, **item) -> SimpleNamespace: +def _done(item_type: OpenAIItemType, output_index: int = 0, **item) -> SimpleNamespace: return _raw( RawResponseEventType.OUTPUT_ITEM_DONE, item=SimpleNamespace(type=item_type, **item), @@ -67,17 +67,17 @@ def _done(item_type: SDKItemType, output_index: int = 0, **item) -> SimpleNamesp def _run_item(item) -> SimpleNamespace: - return SimpleNamespace(type=SDKStreamEventType.RUN_ITEM, name="x", item=item) + return SimpleNamespace(type=OpenAIStreamEventType.RUN_ITEM, name="x", item=item) def _agent_updated(name: str) -> SimpleNamespace: return SimpleNamespace( - type=SDKStreamEventType.AGENT_UPDATED, + type=OpenAIStreamEventType.AGENT_UPDATED, new_agent=SimpleNamespace(name=name), ) -def _drive(engine: SDKToAGUITranslator, *events) -> list: +def _drive(engine: OpenAIToAGUITranslator, *events) -> list: out: list = [] for event in events: out.extend(engine.translate(event)) @@ -92,13 +92,13 @@ def _types(events) -> list[EventType]: def test_text_streams_start_content_end_under_one_id(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() events = _drive( engine, - _added(SDKItemType.MESSAGE, id="msg_1"), + _added(OpenAIItemType.MESSAGE, id="msg_1"), _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="Hel"), _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="lo"), - _done(SDKItemType.MESSAGE, id="msg_1"), + _done(OpenAIItemType.MESSAGE, id="msg_1"), ) assert _types(events) == [ EventType.TEXT_MESSAGE_START, @@ -117,7 +117,7 @@ def test_text_streams_start_content_end_under_one_id(): def test_text_delta_lazily_opens_window_without_output_item_added(): # Some backends jump straight to deltas with no output_item.added first. - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() events = _drive( engine, _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), @@ -126,10 +126,10 @@ def test_text_delta_lazily_opens_window_without_output_item_added(): def test_text_done_closes_window_when_output_item_done_is_skipped(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() events = _drive( engine, - _added(SDKItemType.MESSAGE, id="msg_1"), + _added(OpenAIItemType.MESSAGE, id="msg_1"), _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), _raw(RawResponseEventType.TEXT_DONE, item_id="msg_1", output_index=0), ) @@ -137,10 +137,10 @@ def test_text_done_closes_window_when_output_item_done_is_skipped(): def test_refusal_delta_streams_into_the_text_window(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() events = _drive( engine, - _added(SDKItemType.MESSAGE, id="msg_1"), + _added(OpenAIItemType.MESSAGE, id="msg_1"), _raw(RawResponseEventType.REFUSAL_DELTA, item_id="msg_1", output_index=0, delta="no"), ) assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] @@ -154,12 +154,12 @@ def test_text_less_message_item_wrapping_a_tool_call_emits_no_window(): # output_item.done(message) — with no text delta in between. The message # window must never open, so the tool call stays a clean sibling and no # empty TEXT_MESSAGE_START/END brackets it on the wire. - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() events = _drive( engine, - _added(SDKItemType.MESSAGE, output_index=0, id="msg_1"), + _added(OpenAIItemType.MESSAGE, output_index=0, id="msg_1"), _added( - SDKItemType.FUNCTION_CALL, + OpenAIItemType.FUNCTION_CALL, output_index=1, id="fc_1", call_id="call_1", @@ -172,13 +172,13 @@ def test_text_less_message_item_wrapping_a_tool_call_emits_no_window(): delta='{"background":"red"}', ), _done( - SDKItemType.FUNCTION_CALL, + OpenAIItemType.FUNCTION_CALL, output_index=1, id="fc_1", call_id="call_1", name="change_background", ), - _done(SDKItemType.MESSAGE, output_index=0, id="msg_1"), + _done(OpenAIItemType.MESSAGE, output_index=0, id="msg_1"), ) assert _types(events) == [ EventType.TOOL_CALL_START, @@ -193,10 +193,10 @@ def test_text_less_message_item_wrapping_a_tool_call_emits_no_window(): def test_function_call_streams_start_args_end_under_the_call_id(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() events = _drive( engine, - _added(SDKItemType.FUNCTION_CALL, id="fc_1", call_id="call_1", name="get_weather"), + _added(OpenAIItemType.FUNCTION_CALL, id="fc_1", call_id="call_1", name="get_weather"), _raw( RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, item_id="fc_1", @@ -209,7 +209,7 @@ def test_function_call_streams_start_args_end_under_the_call_id(): output_index=0, delta='"Cairo"}', ), - _done(SDKItemType.FUNCTION_CALL, id="fc_1", call_id="call_1", name="get_weather"), + _done(OpenAIItemType.FUNCTION_CALL, id="fc_1", call_id="call_1", name="get_weather"), ) assert _types(events) == [ EventType.TOOL_CALL_START, @@ -225,7 +225,7 @@ def test_function_call_streams_start_args_end_under_the_call_id(): def test_function_call_args_delta_lazily_opens_the_call(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() events = _drive( engine, _raw( @@ -239,7 +239,7 @@ def test_function_call_args_delta_lazily_opens_the_call(): def test_tool_output_item_emits_tool_call_result(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() item = ToolCallOutputItem( agent=_AGENT, raw_item={"type": "function_call_output", "call_id": "call_1", "output": "sunny"}, @@ -255,7 +255,7 @@ def test_tool_output_item_emits_tool_call_result(): def test_reasoning_summary_delta_opens_phase_and_part(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() events = _drive( engine, _raw( @@ -280,7 +280,7 @@ def test_reasoning_summary_delta_opens_phase_and_part(): def test_reasoning_auto_closes_when_text_output_starts(): # Once real output begins, any open reasoning must be closed first — # reasoning must never bleed into the answer window. - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() _drive( engine, _raw( @@ -293,7 +293,7 @@ def test_reasoning_auto_closes_when_text_output_starts(): # output_item.added closes reasoning right away (output has begun), but # holds back TEXT_MESSAGE_START — that waits for a real delta so a # text-less item can't emit an empty window. - events = _drive(engine, _added(SDKItemType.MESSAGE, id="msg_1")) + events = _drive(engine, _added(OpenAIItemType.MESSAGE, id="msg_1")) assert _types(events) == [ EventType.REASONING_MESSAGE_END, EventType.REASONING_END, @@ -310,7 +310,7 @@ def test_reasoning_auto_closes_when_text_output_starts(): def test_agent_updated_opens_a_step_finalize_closes_it(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() events = _drive(engine, _agent_updated("triage_agent")) assert _types(events) == [EventType.STEP_STARTED] assert events[0].step_name == "triage_agent" @@ -320,7 +320,7 @@ def test_agent_updated_opens_a_step_finalize_closes_it(): def test_handoff_pairs_step_finished_with_the_next_step_started(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() _drive(engine, _agent_updated("triage_agent")) events = _drive(engine, _agent_updated("billing_agent")) assert _types(events) == [EventType.STEP_FINISHED, EventType.STEP_STARTED] @@ -332,7 +332,7 @@ def test_handoff_pairs_step_finished_with_the_next_step_started(): def test_handoff_call_and_output_items_map_to_tool_call_and_result(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() call = HandoffCallItem( agent=_AGENT, raw_item=ResponseFunctionToolCall( @@ -368,7 +368,7 @@ def test_handoff_call_and_output_items_map_to_tool_call_and_result(): def test_mcp_approval_request_maps_to_a_custom_event(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() item = MCPApprovalRequestItem( agent=_AGENT, raw_item=McpApprovalRequest( @@ -389,5 +389,5 @@ def test_mcp_approval_request_maps_to_a_custom_event(): def test_unknown_stream_event_type_translates_to_nothing(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() assert engine.translate(SimpleNamespace(type="something_new", data=None)) == [] diff --git a/integrations/openai-agents/python/tests/test_multimodal.py b/integrations/openai-agents/python/tests/test_multimodal.py index f13ee73170..27cd1318c0 100644 --- a/integrations/openai-agents/python/tests/test_multimodal.py +++ b/integrations/openai-agents/python/tests/test_multimodal.py @@ -1,5 +1,5 @@ """ -Tests for AGUIToSDKTranslator's multimodal content mapping. +Tests for AGUIToOpenAITranslator's multimodal content mapping. Pins the wire shape each ``translate_*_content`` method produces (or the ``None``/drop behavior when unsupported), per the table in @@ -21,9 +21,9 @@ TextInputContent, VideoInputContent, ) -from ag_ui_openai_agents.engine.agui_to_sdk import AGUIToSDKTranslator +from ag_ui_openai_agents.engine.agui_to_openai import AGUIToOpenAITranslator -_engine = AGUIToSDKTranslator() +_engine = AGUIToOpenAITranslator() def _binary(**kwargs) -> BinaryInputContent: diff --git a/integrations/openai-agents/python/tests/test_snapshot.py b/integrations/openai-agents/python/tests/test_snapshot.py index be9a669a5f..030564ed8b 100644 --- a/integrations/openai-agents/python/tests/test_snapshot.py +++ b/integrations/openai-agents/python/tests/test_snapshot.py @@ -1,11 +1,11 @@ """ -Tests for SDKToAGUITranslator.build_messages_snapshot. +Tests for OpenAIToAGUITranslator.build_messages_snapshot. The invariant that matters: every id in the snapshot equals the id the streamed events already used — guaranteed by construction, since the engine resolves each item's id once and hands it to both the streamed event and the accumulated snapshot message. See the snapshot-message -builders in engine/sdk_to_agui.py. +builders in engine/openai_to_agui.py. """ from __future__ import annotations @@ -31,7 +31,7 @@ ToolMessage, UserMessage, ) -from ag_ui_openai_agents.engine import SDKToAGUITranslator +from ag_ui_openai_agents.engine import OpenAIToAGUITranslator _AGENT = Agent(name="test-agent") @@ -87,7 +87,7 @@ def _reasoning_item(item_id: str = "rs_1") -> ReasoningItem: def test_snapshot_ids_match_streamed_event_ids(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() items = [_text_item(), _tool_call_item(), _tool_output_item()] streamed = [] for item in items: @@ -126,7 +126,7 @@ def test_snapshot_ids_match_streamed_event_ids(): def test_tool_call_and_result_round_trip(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() engine.translate_item(_tool_call_item()) engine.translate_item(_tool_output_item()) @@ -159,7 +159,7 @@ def test_snapshot_prepends_input_history_with_original_ids(): context=[], forwarded_props=None, ) - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() engine.translate_item(_text_item("msg_new", "hi again")) event = engine.build_messages_snapshot(run_input) @@ -171,7 +171,7 @@ def test_snapshot_prepends_input_history_with_original_ids(): def test_snapshot_accepts_message_list_and_none_history(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() engine.translate_item(_text_item()) prior = [UserMessage(id="m1", role="user", content="hi")] @@ -187,7 +187,7 @@ def test_snapshot_accepts_message_list_and_none_history(): def test_reasoning_items_are_skipped_from_snapshot(): - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() engine.translate_item(_reasoning_item()) engine.translate_item(_text_item()) @@ -197,7 +197,7 @@ def test_reasoning_items_are_skipped_from_snapshot(): def test_unknown_item_types_are_skipped_not_raised(): from types import SimpleNamespace - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() unknown = SimpleNamespace(raw_item={"type": "something_else"}) events = engine.translate_item(unknown) @@ -219,7 +219,7 @@ def test_unknown_item_types_are_skipped_not_raised(): def test_text_message_closed_by_raw_event_before_commit_still_snapshots(): from types import SimpleNamespace - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() # response.output_item.added then .done — closes the window via the # raw-event path, same as a real streaming run. @@ -247,7 +247,7 @@ def test_text_message_closed_by_raw_event_before_commit_still_snapshots(): def test_tool_call_closed_by_raw_event_before_commit_still_snapshots(): from types import SimpleNamespace - engine = SDKToAGUITranslator() + engine = OpenAIToAGUITranslator() added = SimpleNamespace( item=SimpleNamespace( diff --git a/integrations/openai-agents/python/tests/test_stream_types_drift.py b/integrations/openai-agents/python/tests/test_stream_types_drift.py index ff5737b067..5cdd70628d 100644 --- a/integrations/openai-agents/python/tests/test_stream_types_drift.py +++ b/integrations/openai-agents/python/tests/test_stream_types_drift.py @@ -46,8 +46,8 @@ from ag_ui_openai_agents.engine import ( HOSTED_TOOL_CALL_TYPES, RawResponseEventType, - SDKItemType, - SDKStreamEventType, + OpenAIItemType, + OpenAIStreamEventType, ) @@ -74,12 +74,12 @@ def output_item_union_members() -> tuple[type, ...]: @pytest.mark.parametrize( ("ours", "sdk_cls"), [ - (SDKStreamEventType.RAW_RESPONSE, RawResponsesStreamEvent), - (SDKStreamEventType.RUN_ITEM, RunItemStreamEvent), - (SDKStreamEventType.AGENT_UPDATED, AgentUpdatedStreamEvent), + (OpenAIStreamEventType.RAW_RESPONSE, RawResponsesStreamEvent), + (OpenAIStreamEventType.RUN_ITEM, RunItemStreamEvent), + (OpenAIStreamEventType.AGENT_UPDATED, AgentUpdatedStreamEvent), ], ) -def test_stream_event_types_match_sdk(ours: SDKStreamEventType, sdk_cls: type) -> None: +def test_stream_event_types_match_sdk(ours: OpenAIStreamEventType, sdk_cls: type) -> None: assert ours == dataclass_wire_value(sdk_cls) @@ -116,12 +116,12 @@ def test_raw_response_event_types_match_sdk( @pytest.mark.parametrize( ("ours", "sdk_cls"), [ - (SDKItemType.MESSAGE, ResponseOutputMessage), - (SDKItemType.FUNCTION_CALL, ResponseFunctionToolCall), - (SDKItemType.REASONING, ResponseReasoningItem), + (OpenAIItemType.MESSAGE, ResponseOutputMessage), + (OpenAIItemType.FUNCTION_CALL, ResponseFunctionToolCall), + (OpenAIItemType.REASONING, ResponseReasoningItem), ], ) -def test_item_types_match_sdk(ours: SDKItemType, sdk_cls: type) -> None: +def test_item_types_match_sdk(ours: OpenAIItemType, sdk_cls: type) -> None: assert ours == pydantic_wire_value(sdk_cls) @@ -138,6 +138,6 @@ def test_every_sdk_tool_call_item_type_is_known() -> None: for member in output_item_union_members() if pydantic_wire_value(member).endswith("_call") } - known = HOSTED_TOOL_CALL_TYPES | {SDKItemType.FUNCTION_CALL.value} + known = HOSTED_TOOL_CALL_TYPES | {OpenAIItemType.FUNCTION_CALL.value} unknown = sdk_call_types - known assert not unknown, f"SDK added tool-call item types we don't map: {unknown}" From 531f5b1668545d0691f85dd81a67eecf382fd70c Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 11:30:28 +0200 Subject: [PATCH 50/94] docs(stream_types): update compatibility note for StrEnum to reflect Python 3.10 support --- .../python/src/ag_ui_openai_agents/engine/stream_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py index 91b636b472..272713608e 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py @@ -1,7 +1,7 @@ """Wire-level discriminator values for OpenAI Agents SDK streaming. Single home for every "type" string the translators dispatch on. Members -are (str, Enum) (StrEnum needs Python 3.11; this package supports 3.9+), +are (str, Enum) (StrEnum needs Python 3.11; this package supports 3.10+), so they compare equal to the raw wire strings. The SDK's own Literal[...] annotations are the source of truth — see tests/test_stream_types_drift.py. """ From ec0786f57e118b3bc37e4efd2e6febfb93d9c5fa Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 23:02:32 +0200 Subject: [PATCH 51/94] refactor(openai-agents): rename RawResponseEventType to OpenAIRawResponseEventType --- integrations/openai-agents/python/README.md | 4 +- .../src/ag_ui_openai_agents/__init__.py | 10 +- .../ag_ui_openai_agents/engine/__init__.py | 25 ++-- .../src/ag_ui_openai_agents/engine/helpers.py | 2 +- .../engine/openai_to_agui.py | 24 ++-- .../engine/stream_types.py | 67 ---------- .../src/ag_ui_openai_agents/engine/types.py | 126 ++++++++++++++++-- .../python/tests/test_engine_mapping.py | 38 +++--- .../python/tests/test_stream_types_drift.py | 26 ++-- 9 files changed, 173 insertions(+), 149 deletions(-) delete mode 100644 integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index b327967d6a..7e17032ef5 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -876,11 +876,11 @@ uv run pytest # run the full suite The suite includes a **drift guard** (`tests/test_stream_types_drift.py`): this package hardcodes the wire `type` strings it dispatches on (in -`engine/stream_types.py`), and the guard asserts each one against the +`engine/types.py`), and the guard asserts each one against the `Literal[...]` annotations of the installed `openai-agents` / `openai` packages. After bumping either dependency, run `uv run pytest` — if a wire type was renamed or a new hosted tool-call item type was added, the guard fails with an assertion diff naming the exact value to update in -`stream_types.py`. Unknown types never crash at runtime (the translator +`types.py`. Unknown types never crash at runtime (the translator degrades gracefully and skips them); the guard exists so drift is caught in CI instead of silently dropping events. diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py index ef13e05214..76c700ba43 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py @@ -35,15 +35,11 @@ __version__ = "0.1.0" __all__ = [ - # Serve an agent (highest level) - "OpenAIAgentsAgent", "add_openai_agents_fastapi_endpoint", - # Public translator (primary API — 2 methods) - "AGUITranslator", - # Engine translators (advanced / per-mapping overrides) "AGUIToOpenAITranslator", + "AGUITranslator", + "ClientToolPending", + "OpenAIAgentsAgent", "OpenAIToAGUITranslator", - # Types & helpers "TranslatedInput", - "ClientToolPending", ] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py index e11c6dbda6..fc4b536986 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py @@ -16,31 +16,26 @@ read_attr, ) from .openai_to_agui import OpenAIToAGUITranslator -from .stream_types import ( +from .types import ( HOSTED_TOOL_CALL_TYPES, - RawResponseEventType, OpenAIItemType, + OpenAIRawResponseEventType, OpenAIStreamEventType, + TranslatedInput, ) -from .types import TranslatedInput __all__ = [ - # Engine translators "AGUIToOpenAITranslator", - "OpenAIToAGUITranslator", - # Result types - "TranslatedInput", - # Wire discriminators - "OpenAIStreamEventType", - "RawResponseEventType", - "OpenAIItemType", - "HOSTED_TOOL_CALL_TYPES", - # Sentinels "ClientToolPending", - # Shared helpers + "coerce_to_str", + "HOSTED_TOOL_CALL_TYPES", "new_message_id", "new_tool_call_id", "new_tool_result_id", + "OpenAIItemType", + "OpenAIRawResponseEventType", + "OpenAIStreamEventType", + "OpenAIToAGUITranslator", "read_attr", - "coerce_to_str", + "TranslatedInput", ] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py index 4b54d7bbd2..65ad306305 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py @@ -91,9 +91,9 @@ def coerce_to_str(value: Any) -> str: __all__ = [ + "coerce_to_str", "new_message_id", "new_tool_call_id", "new_tool_result_id", "read_attr", - "coerce_to_str", ] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index e07c2fe6b3..226102292a 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -65,10 +65,10 @@ new_tool_result_id, read_attr, ) -from .stream_types import ( +from .types import ( HOSTED_TOOL_CALL_TYPES, - RawResponseEventType, OpenAIItemType, + OpenAIRawResponseEventType, OpenAIStreamEventType, ) @@ -239,26 +239,26 @@ def translate_raw_response_event(self, event: Any) -> list[BaseEvent]: if data is None: return [] kind = read_attr(data, "type") - if kind == RawResponseEventType.OUTPUT_ITEM_ADDED: + if kind == OpenAIRawResponseEventType.OUTPUT_ITEM_ADDED: return self.translate_output_item_added(data) - if kind == RawResponseEventType.OUTPUT_ITEM_DONE: + if kind == OpenAIRawResponseEventType.OUTPUT_ITEM_DONE: return self.translate_output_item_done(data) - if kind == RawResponseEventType.TEXT_DELTA: + if kind == OpenAIRawResponseEventType.TEXT_DELTA: return self.translate_text_delta(data) - if kind == RawResponseEventType.TEXT_DONE: + if kind == OpenAIRawResponseEventType.TEXT_DONE: return self.translate_text_done(data) - if kind == RawResponseEventType.REFUSAL_DELTA: + if kind == OpenAIRawResponseEventType.REFUSAL_DELTA: return self.translate_refusal_delta(data) - if kind == RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA: + if kind == OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA: return self.translate_function_call_arguments_delta(data) if kind in ( - RawResponseEventType.REASONING_SUMMARY_DELTA, - RawResponseEventType.REASONING_TEXT_DELTA, + OpenAIRawResponseEventType.REASONING_SUMMARY_DELTA, + OpenAIRawResponseEventType.REASONING_TEXT_DELTA, ): return self.translate_reasoning_delta(data) if kind in ( - RawResponseEventType.REASONING_SUMMARY_PART_DONE, - RawResponseEventType.REASONING_TEXT_DONE, + OpenAIRawResponseEventType.REASONING_SUMMARY_PART_DONE, + OpenAIRawResponseEventType.REASONING_TEXT_DONE, ): return self.translate_reasoning_part_done(data) # Everything else (response.created/.completed, content_part chatter, diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py deleted file mode 100644 index 272713608e..0000000000 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/stream_types.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Wire-level discriminator values for OpenAI Agents SDK streaming. - -Single home for every "type" string the translators dispatch on. Members -are (str, Enum) (StrEnum needs Python 3.11; this package supports 3.10+), -so they compare equal to the raw wire strings. The SDK's own Literal[...] -annotations are the source of truth — see tests/test_stream_types_drift.py. -""" - -from __future__ import annotations - -from enum import Enum - - -class OpenAIStreamEventType(str, Enum): - """Top-level StreamEvent.type values yielded by Runner.run_streamed.""" - - RAW_RESPONSE = "raw_response_event" - RUN_ITEM = "run_item_stream_event" - AGENT_UPDATED = "agent_updated_stream_event" - - -class RawResponseEventType(str, Enum): - """Raw Responses-API delta type values the outbound translator consumes. - - Non-semantic bookkeeping kinds (response.created / .completed, - content_part.*, audio) are deliberately absent — they translate to - nothing. - """ - - OUTPUT_ITEM_ADDED = "response.output_item.added" - OUTPUT_ITEM_DONE = "response.output_item.done" - TEXT_DELTA = "response.output_text.delta" - TEXT_DONE = "response.output_text.done" - REFUSAL_DELTA = "response.refusal.delta" - FUNCTION_CALL_ARGUMENTS_DELTA = "response.function_call_arguments.delta" - REASONING_SUMMARY_DELTA = "response.reasoning_summary_text.delta" - REASONING_SUMMARY_PART_DONE = "response.reasoning_summary_part.done" - REASONING_TEXT_DELTA = "response.reasoning_text.delta" - REASONING_TEXT_DONE = "response.reasoning_text.done" - - -class OpenAIItemType(str, Enum): - """Output-item type values carried by output_item.added / .done.""" - - MESSAGE = "message" - FUNCTION_CALL = "function_call" - REASONING = "reasoning" - - -# Tools that run on OpenAI's side. We still show them as AG-UI tool calls, but -# the API doesn't stream their arguments, so at the raw level all we can emit is -# START/END. The run-item layer fills in the rest when it has it. -HOSTED_TOOL_CALL_TYPES: frozenset[str] = frozenset( - { - "web_search_call", - "file_search_call", - "code_interpreter_call", - "image_generation_call", - "computer_call", - "local_shell_call", - "shell_call", - "apply_patch_call", - "mcp_call", - "custom_tool_call", - "tool_search_call", - } -) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index f2eac03079..5487a77826 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -1,12 +1,19 @@ -"""Public data containers for the translator package. - -Holds typed result objects produced by the translators. Translator -behavior lives in agui_to_openai.py and openai_to_agui.py; this module only -describes the shapes those translators hand back. +"""Type definitions for the OpenAI Agents integration. + +Result containers the translators hand back (``TranslatedInput``), plus the +OpenAI wire-level discriminator values the outbound translator dispatches on +(``OpenAIItemType``, ``OpenAIRawResponseEventType``, ``OpenAIStreamEventType``, +``HOSTED_TOOL_CALL_TYPES``). Members of the ``(str, Enum)`` classes compare +equal to the raw wire strings (``StrEnum`` needs Python 3.11; this package +supports 3.10+). The SDK's own ``Literal[...]`` annotations are the source of +truth for those — see ``tests/test_stream_types_drift.py``. Translator +behavior lives in ``agui_to_openai.py`` and ``openai_to_agui.py``; this module +only describes shapes and constants. """ from __future__ import annotations +from enum import Enum from typing import Any from agents import FunctionTool, TResponseInputItem @@ -15,14 +22,43 @@ from ag_ui.core import Context -class TranslatedInput(BaseModel): - """SDK-ready bundle produced by translating an AG-UI RunAgentInput. +# ── ag_ui_openai_agents result types ──────────────────────────────────────── - The fields line up one-for-one with ag_ui.core.RunAgentInput — same - names, same required/optional split — so if you know the wire format - you already know this. The only real work happens on `messages` and - `tools` (translated into SDK shapes); everything else is handed - straight through for you to use however your app needs. +class TranslatedInput(BaseModel): + """OpenAI Agents SDK-ready bundle produced by translating an AG-UI RunAgentInput. + + Returned by ``AGUITranslator.to_openai()`` (or + ``AGUIToOpenAITranslator.translate()`` directly, for advanced/per-mapping + use). Fields line up one-for-one with ``ag_ui.core.RunAgentInput`` — same + names, same required/optional split — so if you know the wire format you + already know this shape. Only ``messages`` and ``tools`` are actually + translated into OpenAI Agents SDK types; everything else passes through + unchanged for you to use however your app needs. + + Example: + translated_input = translator.to_openai(run_input) + result = Runner.run_streamed( + agent, + input=translated_input.messages, + context=translated_input.context, + ) + + Attributes: + thread_id: Conversation key. Stays the same across turns of one thread. + run_id: Id for this single run. Goes on the RUN_STARTED / RUN_FINISHED events. + parent_run_id: Set when this run was kicked off by another run + (nested/branched); None for a top-level run. + messages: Responses-API input items, ready to pass to + ``Runner.run*(input=...)``. + tools: FunctionTool proxies for the client's tools. Merge these with + your agent's own tools before running. + state: Whatever the client sent as state. Any, since AG-UI doesn't + pin a shape. + context: Ambient ``{description, value}`` items from the frontend. + Nothing folds these into the prompt for you. + forwarded_props: Grab-bag of extra client props (model overrides, + temperature, flags, ...) that don't have a dedicated field. + resume: Resume entries when continuing from an interrupt, else None. """ # ── Identity ───────────────────────────────────────────────────────── @@ -76,4 +112,68 @@ class TranslatedInput(BaseModel): TranslatedInput.model_rebuild() -__all__ = ["TranslatedInput"] + +# ── OpenAI wire discriminators (raw events openai-agents emits) ───────────────── + +class OpenAIStreamEventType(str, Enum): + """Top-level StreamEvent.type values yielded by Runner.run_streamed.""" + + RAW_RESPONSE = "raw_response_event" + RUN_ITEM = "run_item_stream_event" + AGENT_UPDATED = "agent_updated_stream_event" + + +class OpenAIRawResponseEventType(str, Enum): + """Raw Responses-API delta type values the outbound translator consumes. + + Non-semantic bookkeeping kinds (response.created / .completed, + content_part.*, audio) are deliberately absent — they translate to + nothing. + """ + + OUTPUT_ITEM_ADDED = "response.output_item.added" + OUTPUT_ITEM_DONE = "response.output_item.done" + TEXT_DELTA = "response.output_text.delta" + TEXT_DONE = "response.output_text.done" + REFUSAL_DELTA = "response.refusal.delta" + FUNCTION_CALL_ARGUMENTS_DELTA = "response.function_call_arguments.delta" + REASONING_SUMMARY_DELTA = "response.reasoning_summary_text.delta" + REASONING_SUMMARY_PART_DONE = "response.reasoning_summary_part.done" + REASONING_TEXT_DELTA = "response.reasoning_text.delta" + REASONING_TEXT_DONE = "response.reasoning_text.done" + + +class OpenAIItemType(str, Enum): + """Output-item type values carried by output_item.added / .done.""" + + MESSAGE = "message" + FUNCTION_CALL = "function_call" + REASONING = "reasoning" + + +# Tools that run on OpenAI's side. We still show them as AG-UI tool calls, but +# the API doesn't stream their arguments, so at the raw level all we can emit is +# START/END. The run-item layer fills in the rest when it has it. +HOSTED_TOOL_CALL_TYPES: frozenset[str] = frozenset( + { + "web_search_call", + "file_search_call", + "code_interpreter_call", + "image_generation_call", + "computer_call", + "local_shell_call", + "shell_call", + "apply_patch_call", + "mcp_call", + "custom_tool_call", + "tool_search_call", + } +) + +__all__ = [ + "HOSTED_TOOL_CALL_TYPES", + "OpenAIItemType", + "OpenAIRawResponseEventType", + "OpenAIStreamEventType", + "TranslatedInput", +] diff --git a/integrations/openai-agents/python/tests/test_engine_mapping.py b/integrations/openai-agents/python/tests/test_engine_mapping.py index 8708889928..dccd34cbb0 100644 --- a/integrations/openai-agents/python/tests/test_engine_mapping.py +++ b/integrations/openai-agents/python/tests/test_engine_mapping.py @@ -30,9 +30,9 @@ from ag_ui.core import EventType from ag_ui_openai_agents.engine import OpenAIToAGUITranslator -from ag_ui_openai_agents.engine.stream_types import ( - RawResponseEventType, +from ag_ui_openai_agents.engine.types import ( OpenAIItemType, + OpenAIRawResponseEventType, OpenAIStreamEventType, ) @@ -42,7 +42,7 @@ # ── event builders — shaped like the SDK's real stream events ──────────── -def _raw(kind: RawResponseEventType, **data) -> SimpleNamespace: +def _raw(kind: OpenAIRawResponseEventType, **data) -> SimpleNamespace: """A RawResponsesStreamEvent wrapping one Responses payload.""" return SimpleNamespace( type=OpenAIStreamEventType.RAW_RESPONSE, @@ -52,7 +52,7 @@ def _raw(kind: RawResponseEventType, **data) -> SimpleNamespace: def _added(item_type: OpenAIItemType, output_index: int = 0, **item) -> SimpleNamespace: return _raw( - RawResponseEventType.OUTPUT_ITEM_ADDED, + OpenAIRawResponseEventType.OUTPUT_ITEM_ADDED, item=SimpleNamespace(type=item_type, **item), output_index=output_index, ) @@ -60,7 +60,7 @@ def _added(item_type: OpenAIItemType, output_index: int = 0, **item) -> SimpleNa def _done(item_type: OpenAIItemType, output_index: int = 0, **item) -> SimpleNamespace: return _raw( - RawResponseEventType.OUTPUT_ITEM_DONE, + OpenAIRawResponseEventType.OUTPUT_ITEM_DONE, item=SimpleNamespace(type=item_type, **item), output_index=output_index, ) @@ -96,8 +96,8 @@ def test_text_streams_start_content_end_under_one_id(): events = _drive( engine, _added(OpenAIItemType.MESSAGE, id="msg_1"), - _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="Hel"), - _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="lo"), + _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="Hel"), + _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="lo"), _done(OpenAIItemType.MESSAGE, id="msg_1"), ) assert _types(events) == [ @@ -120,7 +120,7 @@ def test_text_delta_lazily_opens_window_without_output_item_added(): engine = OpenAIToAGUITranslator() events = _drive( engine, - _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), + _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), ) assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] @@ -130,8 +130,8 @@ def test_text_done_closes_window_when_output_item_done_is_skipped(): events = _drive( engine, _added(OpenAIItemType.MESSAGE, id="msg_1"), - _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), - _raw(RawResponseEventType.TEXT_DONE, item_id="msg_1", output_index=0), + _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), + _raw(OpenAIRawResponseEventType.TEXT_DONE, item_id="msg_1", output_index=0), ) assert _types(events)[-1] == EventType.TEXT_MESSAGE_END @@ -141,7 +141,7 @@ def test_refusal_delta_streams_into_the_text_window(): events = _drive( engine, _added(OpenAIItemType.MESSAGE, id="msg_1"), - _raw(RawResponseEventType.REFUSAL_DELTA, item_id="msg_1", output_index=0, delta="no"), + _raw(OpenAIRawResponseEventType.REFUSAL_DELTA, item_id="msg_1", output_index=0, delta="no"), ) assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] assert events[1].delta == "no" @@ -166,7 +166,7 @@ def test_text_less_message_item_wrapping_a_tool_call_emits_no_window(): name="change_background", ), _raw( - RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, item_id="fc_1", output_index=1, delta='{"background":"red"}', @@ -198,13 +198,13 @@ def test_function_call_streams_start_args_end_under_the_call_id(): engine, _added(OpenAIItemType.FUNCTION_CALL, id="fc_1", call_id="call_1", name="get_weather"), _raw( - RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, item_id="fc_1", output_index=0, delta='{"city":', ), _raw( - RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, item_id="fc_1", output_index=0, delta='"Cairo"}', @@ -229,7 +229,7 @@ def test_function_call_args_delta_lazily_opens_the_call(): events = _drive( engine, _raw( - RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, item_id="fc_1", output_index=0, delta="{}", @@ -259,12 +259,12 @@ def test_reasoning_summary_delta_opens_phase_and_part(): events = _drive( engine, _raw( - RawResponseEventType.REASONING_SUMMARY_DELTA, + OpenAIRawResponseEventType.REASONING_SUMMARY_DELTA, item_id="rs_1", output_index=0, delta="thinking", ), - _raw(RawResponseEventType.REASONING_SUMMARY_PART_DONE, item_id="rs_1", output_index=0), + _raw(OpenAIRawResponseEventType.REASONING_SUMMARY_PART_DONE, item_id="rs_1", output_index=0), ) assert _types(events) == [ EventType.REASONING_START, @@ -284,7 +284,7 @@ def test_reasoning_auto_closes_when_text_output_starts(): _drive( engine, _raw( - RawResponseEventType.REASONING_TEXT_DELTA, + OpenAIRawResponseEventType.REASONING_TEXT_DELTA, item_id="rs_1", output_index=0, delta="hmm", @@ -300,7 +300,7 @@ def test_reasoning_auto_closes_when_text_output_starts(): ] events = _drive( engine, - _raw(RawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="ok"), + _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="ok"), ) assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] assert events[0].message_id == "msg_1", "deferred START must carry the reserved id" diff --git a/integrations/openai-agents/python/tests/test_stream_types_drift.py b/integrations/openai-agents/python/tests/test_stream_types_drift.py index 5cdd70628d..cb6216d4f7 100644 --- a/integrations/openai-agents/python/tests/test_stream_types_drift.py +++ b/integrations/openai-agents/python/tests/test_stream_types_drift.py @@ -1,7 +1,7 @@ """ Drift guard: our wire strings must match the installed SDK's own types. -``stream_types.py`` hardcodes the discriminator strings the translators +``types.py`` hardcodes the discriminator strings the translators dispatch on. The SDK declares the same strings as ``Literal[...]`` annotations on its event/item classes. Nothing compares the two at runtime — if the SDK renamed a wire value, dispatch would silently stop matching (graceful @@ -45,8 +45,8 @@ from ag_ui_openai_agents.engine import ( HOSTED_TOOL_CALL_TYPES, - RawResponseEventType, OpenAIItemType, + OpenAIRawResponseEventType, OpenAIStreamEventType, ) @@ -86,29 +86,29 @@ def test_stream_event_types_match_sdk(ours: OpenAIStreamEventType, sdk_cls: type @pytest.mark.parametrize( ("ours", "sdk_cls"), [ - (RawResponseEventType.OUTPUT_ITEM_ADDED, ResponseOutputItemAddedEvent), - (RawResponseEventType.OUTPUT_ITEM_DONE, ResponseOutputItemDoneEvent), - (RawResponseEventType.TEXT_DELTA, ResponseTextDeltaEvent), - (RawResponseEventType.TEXT_DONE, ResponseTextDoneEvent), - (RawResponseEventType.REFUSAL_DELTA, ResponseRefusalDeltaEvent), + (OpenAIRawResponseEventType.OUTPUT_ITEM_ADDED, ResponseOutputItemAddedEvent), + (OpenAIRawResponseEventType.OUTPUT_ITEM_DONE, ResponseOutputItemDoneEvent), + (OpenAIRawResponseEventType.TEXT_DELTA, ResponseTextDeltaEvent), + (OpenAIRawResponseEventType.TEXT_DONE, ResponseTextDoneEvent), + (OpenAIRawResponseEventType.REFUSAL_DELTA, ResponseRefusalDeltaEvent), ( - RawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, ResponseFunctionCallArgumentsDeltaEvent, ), ( - RawResponseEventType.REASONING_SUMMARY_DELTA, + OpenAIRawResponseEventType.REASONING_SUMMARY_DELTA, ResponseReasoningSummaryTextDeltaEvent, ), ( - RawResponseEventType.REASONING_SUMMARY_PART_DONE, + OpenAIRawResponseEventType.REASONING_SUMMARY_PART_DONE, ResponseReasoningSummaryPartDoneEvent, ), - (RawResponseEventType.REASONING_TEXT_DELTA, ResponseReasoningTextDeltaEvent), - (RawResponseEventType.REASONING_TEXT_DONE, ResponseReasoningTextDoneEvent), + (OpenAIRawResponseEventType.REASONING_TEXT_DELTA, ResponseReasoningTextDeltaEvent), + (OpenAIRawResponseEventType.REASONING_TEXT_DONE, ResponseReasoningTextDoneEvent), ], ) def test_raw_response_event_types_match_sdk( - ours: RawResponseEventType, sdk_cls: type + ours: OpenAIRawResponseEventType, sdk_cls: type ) -> None: assert ours == pydantic_wire_value(sdk_cls) From f2a2b56fa747d827f1f81100b2f538483983cef1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 23:09:23 +0200 Subject: [PATCH 52/94] refactor(openai-agents): drop unnecessary from __future__ import annotations --- .../openai-agents/python/src/ag_ui_openai_agents/__init__.py | 2 -- .../openai-agents/python/src/ag_ui_openai_agents/agent.py | 2 -- .../openai-agents/python/src/ag_ui_openai_agents/endpoint.py | 2 -- .../python/src/ag_ui_openai_agents/engine/__init__.py | 2 -- .../python/src/ag_ui_openai_agents/engine/agui_to_openai.py | 2 -- .../python/src/ag_ui_openai_agents/engine/helpers.py | 2 -- .../python/src/ag_ui_openai_agents/engine/openai_to_agui.py | 2 -- .../openai-agents/python/src/ag_ui_openai_agents/translator.py | 2 -- 8 files changed, 16 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py index 76c700ba43..79b8558d1c 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py @@ -20,8 +20,6 @@ from ag_ui_openai_agents.engine import AGUIToOpenAITranslator, OpenAIToAGUITranslator """ -from __future__ import annotations - from .agent import OpenAIAgentsAgent from .endpoint import add_openai_agents_fastapi_endpoint from .engine import ( diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index dd6a4b48d2..b1bcb51d79 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -4,8 +4,6 @@ ready for add_openai_agents_fastapi_endpoint. """ -from __future__ import annotations - from typing import Any, AsyncIterator, Callable from agents import Agent, RunConfig, Runner diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py index 4d3a22a652..f33ce4b905 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py @@ -4,8 +4,6 @@ negotiation, health check. """ -from __future__ import annotations - import logging from fastapi import FastAPI, Request diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py index fc4b536986..536fa2aa2a 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py @@ -5,8 +5,6 @@ everything else, import AGUITranslator from the package root instead. """ -from __future__ import annotations - from .agui_to_openai import AGUIToOpenAITranslator, ClientToolPending from .helpers import ( coerce_to_str, diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index 5d5e8fd0ac..ca70bed77b 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -6,8 +6,6 @@ mapping). Stateless — make one and reuse it, or make one per request. """ -from __future__ import annotations - import logging from typing import Any, Iterable diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py index 65ad306305..530ae81a22 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py @@ -4,8 +4,6 @@ can be reused by either translator without coupling them. """ -from __future__ import annotations - import json import uuid from typing import Any diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index 226102292a..2f508b0f0d 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -10,8 +10,6 @@ — the translator only translates. """ -from __future__ import annotations - import logging from typing import Any, Sequence diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 5bddabe764..df3b298d34 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -1,7 +1,5 @@ """Translate between AG-UI requests and OpenAI Agents SDK streams.""" -from __future__ import annotations - import asyncio import inspect from typing import Any, AsyncIterator From 13ec60ae2aea45c1a4c4642d491407f62f23f113 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Tue, 14 Jul 2026 23:55:14 +0200 Subject: [PATCH 53/94] docs(openai-agents): update README and __init__.py for improved clarity and structure --- integrations/openai-agents/python/README.md | 24 ++++++++++++-- .../src/ag_ui_openai_agents/__init__.py | 31 +++---------------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 7e17032ef5..e76bdbba4f 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -178,11 +178,21 @@ branching)? Use the translator directly — see the section above. ## API overview -Two layers, pick by how much control you want: +Everything you need is importable from the package root: + +```python +from ag_ui_openai_agents import ( + AGUITranslator, + OpenAIAgentsAgent, + add_openai_agents_fastapi_endpoint, + TranslatedInput, +) +``` | Name | Kind | Use it for | |---|---|---| | `AGUITranslator` | translator (recommended) | compose it yourself — `to_openai` + `to_agui`; full control of the agent and server | +| `TranslatedInput` | result type | what `translator.to_openai(...)` returns — `messages`/`tools` plus passthrough fields | | `OpenAIAgentsAgent` | wrapper class | serve an agent: `run(RunAgentInput) -> AsyncIterator[BaseEvent]` | | `add_openai_agents_fastapi_endpoint(app, agent, path)` | helper | wire a wrapped agent to FastAPI (SSE + `/health`) | @@ -190,6 +200,11 @@ The wrapper is built on the translator; it trades control for less code. The translator is just an events translator — it does not own your agent or your server, so start there unless the wrapper's shortcut fits as-is. +Everything else (`AGUIToOpenAITranslator`, `OpenAIToAGUITranslator`, +`ClientToolPending`, and the per-type `translate_*` override points) is the +advanced engine layer — import it from `ag_ui_openai_agents.engine`, not the +package root; see [Advanced: the engine layer](#advanced-the-engine-layer). + `AGUITranslator` pairs with the SDK's streaming run mode: | Class | Pairs with | `to_agui(...)` returns | @@ -236,7 +251,8 @@ AGUITranslator(*, inbound_cls=AGUIToOpenAITranslator, outbound_cls=OpenAIToAGUIT ``` These are advanced extension points for changing one mapping without forking -the public orchestration: +the public orchestration. Both defaults live in `ag_ui_openai_agents.engine`, +not the package root: | Parameter | Default | Meaning | |---|---|---| @@ -244,7 +260,9 @@ the public orchestration: | `outbound_cls` | `OpenAIToAGUITranslator` | Class used for SDK stream → AG-UI event translation. A fresh instance is created for every run because it tracks open stream windows. | For normal use, pass neither parameter. For a custom mapping, subclass the -relevant engine class; see [Advanced: the engine layer](#advanced-the-engine-layer). +relevant engine class (`from ag_ui_openai_agents.engine import +AGUIToOpenAITranslator, OpenAIToAGUITranslator`); see +[Advanced: the engine layer](#advanced-the-engine-layer). #### `to_openai(run_input)` diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py index 79b8558d1c..0ca55540ba 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/__init__.py @@ -1,43 +1,20 @@ """OpenAI Agents SDK × AG-UI Protocol integration. -Serve an agent (highest level — wrapper + FastAPI helper): - - from agents import Agent - from ag_ui_openai_agents import ( - OpenAIAgentsAgent, - add_openai_agents_fastapi_endpoint, - ) - - agent = OpenAIAgentsAgent(Agent(name="assistant", instructions="...")) - add_openai_agents_fastapi_endpoint(app, agent, "/") - -Compose it yourself (mid level — the streaming translator): - - from ag_ui_openai_agents import AGUITranslator - -Advanced (per-mapping overrides) — the engine layer: - - from ag_ui_openai_agents.engine import AGUIToOpenAITranslator, OpenAIToAGUITranslator +Compose it yourself (recommended, full control of the agent and server): AGUITranslator, TranslatedInput. +Serve a fixed agent instead (less code, less control): OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint. +Need a custom mapping? Subclass a translator in ag_ui_openai_agents.engine. """ from .agent import OpenAIAgentsAgent from .endpoint import add_openai_agents_fastapi_endpoint -from .engine import ( - AGUIToOpenAITranslator, - ClientToolPending, - OpenAIToAGUITranslator, - TranslatedInput, -) +from .engine import TranslatedInput from .translator import AGUITranslator __version__ = "0.1.0" __all__ = [ "add_openai_agents_fastapi_endpoint", - "AGUIToOpenAITranslator", "AGUITranslator", - "ClientToolPending", "OpenAIAgentsAgent", - "OpenAIToAGUITranslator", "TranslatedInput", ] From ca5c77990936405163817a55dad8698a70b2934d Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 15 Jul 2026 00:07:23 +0200 Subject: [PATCH 54/94] refactor(openai-agents): rename coerce_to_str to to_string for clarity --- .../python/src/ag_ui_openai_agents/engine/__init__.py | 4 ++-- .../python/src/ag_ui_openai_agents/engine/agui_to_openai.py | 4 ++-- .../python/src/ag_ui_openai_agents/engine/helpers.py | 4 ++-- .../python/src/ag_ui_openai_agents/engine/openai_to_agui.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py index 536fa2aa2a..d8db06728c 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py @@ -7,11 +7,11 @@ from .agui_to_openai import AGUIToOpenAITranslator, ClientToolPending from .helpers import ( - coerce_to_str, new_message_id, new_tool_call_id, new_tool_result_id, read_attr, + to_string, ) from .openai_to_agui import OpenAIToAGUITranslator from .types import ( @@ -25,7 +25,6 @@ __all__ = [ "AGUIToOpenAITranslator", "ClientToolPending", - "coerce_to_str", "HOSTED_TOOL_CALL_TYPES", "new_message_id", "new_tool_call_id", @@ -35,5 +34,6 @@ "OpenAIStreamEventType", "OpenAIToAGUITranslator", "read_attr", + "to_string", "TranslatedInput", ] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index ca70bed77b..d7607868a6 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -31,7 +31,7 @@ UserMessage, VideoInputContent, ) -from .helpers import coerce_to_str, read_attr +from .helpers import read_attr, to_string from .types import TranslatedInput logger = logging.getLogger(__name__) @@ -480,7 +480,7 @@ def translate_content(self, content: Any) -> list[dict[str, Any]]: parts.append(converted) if parts: return parts - return [{"type": "input_text", "text": coerce_to_str(content)}] + return [{"type": "input_text", "text": to_string(content)}] def translate_content_part(self, part: Any) -> dict[str, Any] | None: """Dispatch one content part to its per-type translator. diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py index 530ae81a22..78930052df 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py @@ -69,7 +69,7 @@ def read_attr(obj: Any, name: str) -> Any: return getattr(obj, name, None) -def coerce_to_str(value: Any) -> str: +def to_string(value: Any) -> str: """Best-effort stringification for tool outputs and content payloads. Args: @@ -89,9 +89,9 @@ def coerce_to_str(value: Any) -> str: __all__ = [ - "coerce_to_str", "new_message_id", "new_tool_call_id", "new_tool_result_id", "read_attr", + "to_string", ] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index 2f508b0f0d..de4634feb5 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -56,12 +56,12 @@ ToolMessage, ) from .helpers import ( - coerce_to_str, new_message_id, new_reasoning_id, new_tool_call_id, new_tool_result_id, read_attr, + to_string, ) from .types import ( HOSTED_TOOL_CALL_TYPES, @@ -654,7 +654,7 @@ def translate_tool_call_output_item(self, item: ToolCallOutputItem) -> list[Base if not call_id: logger.debug("Tool output without call_id; skipping TOOL_CALL_RESULT") return [] - content = coerce_to_str(item.output) + content = to_string(item.output) result_id = self._record_result(call_id, content) return [ ToolCallResultEvent( @@ -738,7 +738,7 @@ def translate_handoff_output_item(self, item: HandoffOutputItem) -> list[BaseEve return [] target = read_attr(item.target_agent, "name") or "" output = read_attr(raw, "output") or f"Handed off to {target}" - content = coerce_to_str(output) + content = to_string(output) result_id = self._record_result(call_id, content) return [ ToolCallResultEvent( From c0f1ae5bb26bc8642f671340d98cfd95da20bf41 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 15 Jul 2026 00:16:59 +0200 Subject: [PATCH 55/94] refactor(openai-agents): move ClientToolPending class to types.py and clean up imports --- .../ag_ui_openai_agents/engine/__init__.py | 3 +- .../engine/agui_to_openai.py | 39 +------------------ .../src/ag_ui_openai_agents/engine/types.py | 36 +++++++++++++++++ .../src/ag_ui_openai_agents/translator.py | 4 +- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py index d8db06728c..4ce722ca92 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/__init__.py @@ -5,7 +5,7 @@ everything else, import AGUITranslator from the package root instead. """ -from .agui_to_openai import AGUIToOpenAITranslator, ClientToolPending +from .agui_to_openai import AGUIToOpenAITranslator from .helpers import ( new_message_id, new_tool_call_id, @@ -15,6 +15,7 @@ ) from .openai_to_agui import OpenAIToAGUITranslator from .types import ( + ClientToolPending, HOSTED_TOOL_CALL_TYPES, OpenAIItemType, OpenAIRawResponseEventType, diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index d7607868a6..be69eb8d87 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -10,7 +10,6 @@ from typing import Any, Iterable from agents import FunctionTool, RunContextWrapper, TResponseInputItem -from agents.exceptions import AgentsException from ag_ui.core import ( ActivityMessage, @@ -32,46 +31,11 @@ VideoInputContent, ) from .helpers import read_attr, to_string -from .types import TranslatedInput +from .types import ClientToolPending, TranslatedInput logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- -# Sentinel exception used by client-tool proxies -# --------------------------------------------------------------------------- - -class ClientToolPending(AgentsException): - """Raised by a client-tool proxy to signal "stop, the UI owns this call". - - The outer run loop catches it, cancels the SDK run after the current - turn, and persists the resulting RunState keyed by thread_id so the - next AG-UI request (which carries an AG-UI ToolMessage with the - client's result) can resume from the same point. - - Subclasses AgentsException deliberately: the SDK's own tool executor - (agents.run_internal.tool_execution._run_single_tool) special-cases - `isinstance(e, AgentsException)` to re-raise it as-is — anything else - gets wrapped in a generic UserError first, which would hide this from - the outer run loop's `except ClientToolPending` and turn every - client-owned tool call into a hard run failure instead of a clean - hand-off. - - Args: - tool_name: Name of the client-owned tool that was called. - tool_call_id: The SDK's call_id for this invocation. - arguments: Raw JSON arguments string the model produced. - """ - - def __init__(self, tool_name: str, tool_call_id: str, arguments: str) -> None: - super().__init__( - f"Client tool '{tool_name}' (call_id={tool_call_id}) pending UI execution" - ) - self.tool_name = tool_name - self.tool_call_id = tool_call_id - self.arguments = arguments - - # --------------------------------------------------------------------------- # The translator # --------------------------------------------------------------------------- @@ -787,5 +751,4 @@ def _binary_as_file( __all__ = [ "AGUIToOpenAITranslator", - "ClientToolPending", ] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index 5487a77826..9f3337ca18 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -17,6 +17,7 @@ from typing import Any from agents import FunctionTool, TResponseInputItem +from agents.exceptions import AgentsException from pydantic import BaseModel, ConfigDict, SkipValidation from ag_ui.core import Context @@ -170,7 +171,42 @@ class OpenAIItemType(str, Enum): } ) + +# ── Sentinel exception used by client-tool proxies ────────────────────────── + +class ClientToolPending(AgentsException): + """Raised by a client-tool proxy to signal "stop, the UI owns this call". + + The outer run loop catches it, cancels the SDK run after the current + turn, and persists the resulting RunState keyed by thread_id so the + next AG-UI request (which carries an AG-UI ToolMessage with the + client's result) can resume from the same point. + + Subclasses AgentsException deliberately: the SDK's own tool executor + (agents.run_internal.tool_execution._run_single_tool) special-cases + `isinstance(e, AgentsException)` to re-raise it as-is — anything else + gets wrapped in a generic UserError first, which would hide this from + the outer run loop's `except ClientToolPending` and turn every + client-owned tool call into a hard run failure instead of a clean + hand-off. + + Args: + tool_name: Name of the client-owned tool that was called. + tool_call_id: The SDK's call_id for this invocation. + arguments: Raw JSON arguments string the model produced. + """ + + def __init__(self, tool_name: str, tool_call_id: str, arguments: str) -> None: + super().__init__( + f"Client tool '{tool_name}' (call_id={tool_call_id}) pending UI execution" + ) + self.tool_name = tool_name + self.tool_call_id = tool_call_id + self.arguments = arguments + + __all__ = [ + "ClientToolPending", "HOSTED_TOOL_CALL_TYPES", "OpenAIItemType", "OpenAIRawResponseEventType", diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index df3b298d34..aadd17117e 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -17,9 +17,9 @@ RunStartedEvent, StateSnapshotEvent, ) -from .engine.agui_to_openai import AGUIToOpenAITranslator, ClientToolPending +from .engine.agui_to_openai import AGUIToOpenAITranslator from .engine.openai_to_agui import OpenAIToAGUITranslator -from .engine.types import TranslatedInput +from .engine.types import ClientToolPending, TranslatedInput class AGUITranslator: From a7124b973b98de3dff7fe77194a5e8ff12464f18 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 15 Jul 2026 03:56:13 +0200 Subject: [PATCH 56/94] refactor(openai-agents): polish agui_to_openai translator --- integrations/openai-agents/python/README.md | 6 +- .../python/examples/pyproject.toml | 2 +- .../openai-agents/python/examples/uv.lock | 6 +- .../openai-agents/python/pyproject.toml | 2 +- .../engine/agui_to_openai.py | 251 ++++++++---------- .../python/tests/test_translators.py | 15 +- integrations/openai-agents/python/uv.lock | 4 +- 7 files changed, 130 insertions(+), 156 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index e76bdbba4f..692765ebaf 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -556,9 +556,9 @@ aiohttp, raw ASGI, WebSockets, or tests can all use the same translator calls. Both directions, source of truth is `engine/agui_to_openai.py` and `engine/openai_to_agui.py`. -**Inbound** — AG-UI `Message` → Responses-API input item (`AGUIToOpenAITranslator`): +**Inbound** — AG-UI `Message` → OpenAI Agents SDK input item (`AGUIToOpenAITranslator`): -| AG-UI type | SDK / Responses-API shape | +| AG-UI type | OpenAI Agents SDK input shape | |---|---| | `UserMessage` | `{"type": "message", "role": "user", "content": [...]}` (multimodal-aware) | | `SystemMessage` | `{"type": "message", "role": "system", ...}` | @@ -566,7 +566,7 @@ Both directions, source of truth is `engine/agui_to_openai.py` and | `AssistantMessage` | optional text `message` item + one `function_call` item per `tool_calls` entry | | `ToolMessage` | `{"type": "function_call_output", "call_id": ..., "output": ...}` | | `ReasoningMessage` | `{"type": "reasoning", ...}` if `encrypted_value` is set, else dropped (plaintext reasoning isn't replayable) | -| `ActivityMessage` | dropped (no Responses-API equivalent; override to fold into the prompt) | +| `ActivityMessage` | dropped (neither Responses nor Chat Completions has an equivalent model-input item; subclasses may override this mapping) | | `Tool` (client-declared) | SDK `FunctionTool` proxy that raises `ClientToolPending` when invoked | | Content parts: `TextInputContent`, `ImageInputContent`, `AudioInputContent`, `DocumentInputContent`, `BinaryInputContent` | `input_text` / `input_image` / `input_audio` / `input_file` parts | | `VideoInputContent` | dropped (no OpenAI API accepts video input yet) | diff --git a/integrations/openai-agents/python/examples/pyproject.toml b/integrations/openai-agents/python/examples/pyproject.toml index d57c2e2073..ddcf3c6d6c 100644 --- a/integrations/openai-agents/python/examples/pyproject.toml +++ b/integrations/openai-agents/python/examples/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "ag-ui-openai-agents", - "ag-ui-protocol>=0.1.18", + "ag-ui-protocol>=0.1.19", "fastapi>=0.115.2", "uvicorn[standard]>=0.35.0", ] diff --git a/integrations/openai-agents/python/examples/uv.lock b/integrations/openai-agents/python/examples/uv.lock index f1a7a5cc2e..86f5766de0 100644 --- a/integrations/openai-agents/python/examples/uv.lock +++ b/integrations/openai-agents/python/examples/uv.lock @@ -24,7 +24,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "ag-ui-protocol", specifier = ">=0.1.18" }, + { name = "ag-ui-protocol", specifier = ">=0.1.19" }, { name = "fastapi", specifier = ">=0.115.12" }, { name = "openai-agents", specifier = ">=0.8.4" }, { name = "pydantic", specifier = ">=2.13.4" }, @@ -48,7 +48,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "ag-ui-openai-agents", editable = "../" }, - { name = "ag-ui-protocol", specifier = ">=0.1.18" }, + { name = "ag-ui-protocol", specifier = ">=0.1.19" }, { name = "fastapi", specifier = ">=0.115.2" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" }, ] @@ -394,7 +394,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml index 7655dd77c6..957c4c9d19 100644 --- a/integrations/openai-agents/python/pyproject.toml +++ b/integrations/openai-agents/python/pyproject.toml @@ -8,7 +8,7 @@ authors = [ ] requires-python = ">=3.10" dependencies = [ - "ag-ui-protocol>=0.1.18", + "ag-ui-protocol>=0.1.19", "openai-agents>=0.8.4", "pydantic>=2.13.4", "fastapi>=0.115.12", diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index be69eb8d87..4613515321 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -1,10 +1,4 @@ -"""Inbound translator: AG-UI request primitives → OpenAI Agents SDK formats. - -Layered so you can grab as much or as little as you need: translate() does -the whole request in one call, translate_() handles a collection, -and translate_() does a single item (override one to tweak just that -mapping). Stateless — make one and reuse it, or make one per request. -""" +"""Translate AG-UI request data into OpenAI Agents SDK input types.""" import logging from typing import Any, Iterable @@ -36,58 +30,50 @@ logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- -# The translator -# --------------------------------------------------------------------------- - class AGUIToOpenAITranslator: - """Translate AG-UI inbound primitives into OpenAI Agents SDK shapes. + """Map AG-UI request data to OpenAI Agents SDK input types. + + This stateless inbound engine powers ``AGUITranslator.to_openai()``. + Its public ``translate_*`` methods are mapping-level override points for + applications that need to customize individual conversions. Example: - One-shot: + Public translator: - translated_input = AGUIToOpenAITranslator().translate(run_input) + translated_input = AGUITranslator().to_openai(run_input) result = Runner.run_streamed( agent.clone(tools=agent.tools + translated_input.tools), input=translated_input.messages, ) - Per-item: + Advanced per-item mapping: translator = AGUIToOpenAITranslator() - items = [translator.translate_user_message(msg) for msg in user_msgs] - proxy = translator.translate_tool(my_tool) - image_part = translator.translate_image_content(part) + translated_message = translator.translate_user_message(message) """ # ───────────────────────────────────────────────────────────────────── - # TIER 1 — One-shot entry point + # LEVEL 1 — Complete request translation # ───────────────────────────────────────────────────────────────────── def translate(self, run_input: RunAgentInput) -> TranslatedInput: - """Translate an entire RunAgentInput into an SDK-ready bundle. + """Translate a complete AG-UI request for the OpenAI Agents SDK. - This is the high-level entry point. It does everything: converts - messages, wraps tools, and forwards state / context / - forwarded_props / thread_id / run_id / parent_run_id / resume so - callers have one object that mirrors ag_ui.core.RunAgentInput - field-for-field. + Converts message history and client-declared tools while preserving + the request identifiers, state, context, forwarded props, and resume + entries in a ``TranslatedInput`` result. - context and resume both pass through unchanged. context items - (from useCopilotReadable etc.) are not auto-folded into the - system prompt — call translate_context to format them if your - agent needs the model to see them. + Context and resume entries pass through unchanged.Context is not + automatically added to model input; use ``translate_context()`` when + the model should receive it. Args: run_input: The incoming AG-UI RunAgentInput. Returns: - TranslatedInput with translated messages and passthrough - state/context/forwarded_props. + OpenAI Agents SDK inputs and preserved AG-UI request metadata. """ - # Not every version of RunAgentInput has `resume`, so reach for it with - # getattr instead of assuming it's there. - resume = getattr(run_input, "resume", None) + resume = run_input.resume return TranslatedInput( thread_id=run_input.thread_id, run_id=run_input.run_id, @@ -101,20 +87,20 @@ def translate(self, run_input: RunAgentInput) -> TranslatedInput: ) # ───────────────────────────────────────────────────────────────────── - # TIER 2 — Bulk collections + # LEVEL 2 — Collection translation # ───────────────────────────────────────────────────────────────────── def translate_messages( self, messages: Iterable[Message], ) -> list[TResponseInputItem]: - """Translate every message and flatten into one Responses-API input list. + """Translate message history into OpenAI Agents SDK input items. Args: messages: AG-UI messages to translate. Returns: - Flattened list of Responses-API input items. + Flattened SDK input-item list in the original history order. """ items = [] for message in messages: @@ -125,13 +111,16 @@ def translate_tools( self, tools: Iterable[AGUITool], ) -> list[FunctionTool]: - """Wrap every AG-UI tool as a long-running SDK FunctionTool proxy. + """Translate tools declared in ``RunAgentInput.tools``. + + Each client-declared definition becomes an OpenAI Agents SDK ``FunctionTool`` proxy. + These are request tools, not historical ``ToolMessage`` instances. Args: - tools: AG-UI client-declared tools. + tools: Client-declared tools from the AG-UI request. Returns: - SDK FunctionTool proxies, one per input tool. + One SDK ``FunctionTool`` proxy per request tool. """ return [self.translate_tool(tool) for tool in tools] @@ -139,21 +128,10 @@ def translate_context( self, items: Iterable[Context], ) -> str: - """Render ambient context items as a plain-text block for the system prompt. - - AG-UI context carries {description, value} pairs that frontends - (CopilotKit's useCopilotReadable, etc.) send to give the model - ambient knowledge about the user's UI state — current page, - selected item, user identity, etc. - - This does not auto-inject anywhere — callers decide whether to - prepend the rendered string to a system message, store it in - agent state, or ignore it. + """Render AG-UI context as text that callers may add to model input. - The output format is one "Description: value" line per item: - - Description A: value A - Description B: value B + This method does not inject context automatically. Each non-empty + item is rendered as one ``description: value`` line. Example: prompt = "You are helpful." @@ -175,101 +153,124 @@ def translate_context( return "\n".join(lines) # ───────────────────────────────────────────────────────────────────── - # TIER 3a — Single message (dispatcher + per-type) + # LEVEL 3a — Message translation # ───────────────────────────────────────────────────────────────────── def translate_message(self, message: Message) -> list[dict[str, Any]]: - """Dispatch one AG-UI message to the right per-type translator. + """Dispatch one AG-UI message to its type-specific translator. - Returns a list because one message can produce multiple - Responses-API items (an AssistantMessage with N tool_calls - splits into 1 message item + N function_call items). + One message may produce multiple SDK input items; for example, an + ``AssistantMessage`` may contain text and multiple tool calls. Args: message: An AG-UI message of any supported type. Returns: - Zero or more Responses-API input items. + Zero or more OpenAI Agents SDK input items. """ - if isinstance(message, UserMessage): - return [self.translate_user_message(message)] if isinstance(message, SystemMessage): return [self.translate_system_message(message)] if isinstance(message, DeveloperMessage): return [self.translate_developer_message(message)] + if isinstance(message, UserMessage): + return [self.translate_user_message(message)] + if isinstance(message, ReasoningMessage): + item = self.translate_reasoning_message(message) + return [item] if item is not None else [] if isinstance(message, AssistantMessage): return self.translate_assistant_message(message) if isinstance(message, ToolMessage): return [self.translate_tool_message(message)] - if isinstance(message, ReasoningMessage): - item = self.translate_reasoning_message(message) - return [item] if item is not None else [] if isinstance(message, ActivityMessage): item = self.translate_activity_message(message) return [item] if item is not None else [] logger.warning("Unknown AG-UI message type: %s", type(message).__name__) return [] - def translate_user_message(self, message: UserMessage) -> dict[str, Any]: - """Translate a user turn into a Responses-API message item. - - Supports multimodal: message.content may be a string or a list - of typed content parts (text / image / audio / ...). + def translate_system_message(self, message: SystemMessage) -> dict[str, Any]: + """Translate an AG-UI system prompt into an SDK input item. Args: - message: The AG-UI user message. + message: The AG-UI system message. Returns: - {"type": "message", "role": "user", "content": [...]} + {"type": "message", "role": "system", ...} """ return { "type": "message", - "role": "user", - "content": self.translate_content(message.content), + "role": "system", + "content": [{"type": "input_text", "text": message.content or ""}], } - def translate_system_message(self, message: SystemMessage) -> dict[str, Any]: - """Translate a system prompt into a Responses-API message item. + def translate_developer_message(self, message: DeveloperMessage) -> dict[str, Any]: + """Translate an AG-UI developer prompt into an SDK input item. Args: - message: The AG-UI system message. + message: The AG-UI developer message. Returns: - {"type": "message", "role": "system", ...} + {"type": "message", "role": "developer", ...} """ return { "type": "message", - "role": "system", + "role": "developer", "content": [{"type": "input_text", "text": message.content or ""}], } - def translate_developer_message(self, message: DeveloperMessage) -> dict[str, Any]: - """Translate a developer prompt into a Responses-API message item. + def translate_user_message(self, message: UserMessage) -> dict[str, Any]: + """Translate an AG-UI user turn into an SDK input item. - OpenAI's newer model lineage (GPT-4o+, o1) accepts role: - developer as a higher-priority alternative to system. + Content may be text or a list of typed multimodal parts. Args: - message: The AG-UI developer message. + message: The AG-UI user message. Returns: - {"type": "message", "role": "developer", ...} + {"type": "message", "role": "user", "content": [...]} """ return { "type": "message", - "role": "developer", - "content": [{"type": "input_text", "text": message.content or ""}], + "role": "user", + "content": self.translate_content(message.content), + } + + def translate_reasoning_message( + self, + message: ReasoningMessage, + ) -> dict[str, Any] | None: + """Translate replayable AG-UI reasoning into an SDK input item. + + Reasoning is replayed only when ``encrypted_value`` is available. + Plaintext-only reasoning is dropped because it cannot restore the + model's reasoning state. + + Args: + message: The AG-UI reasoning message. + + Returns: + A reasoning input item, or ``None`` when replay is unavailable. + """ + if not message.encrypted_value: + logger.debug( + "Dropping ReasoningMessage id=%s: no encrypted_value to re-ingest", + message.id, + ) + return None + return { + "type": "reasoning", + "id": message.id, + "encrypted_content": message.encrypted_value, + "summary": [{"type": "summary_text", "text": message.content or ""}], } def translate_assistant_message( self, message: AssistantMessage, ) -> list[dict[str, Any]]: - """Translate an assistant turn into message + function_call items. + """Translate an assistant turn into SDK message and function-call items. - The Responses API splits assistant tool calls into separate - items (one message for any spoken text, plus one function_call - per invocation) — so one AG-UI message can produce N+1 items. + Assistant text becomes an input message, while each requested tool + invocation becomes a separate function-call item. Args: message: The AG-UI assistant message. @@ -281,22 +282,12 @@ def translate_assistant_message( items: list[dict[str, Any]] = [] text = message.content or "" if text: + # Keep prior assistant text as an EasyInputMessageParam. + # Adding type="message" routes it to the SDK output-message converter. items.append( { "role": "assistant", "content": text, - # Deliberately no "type" key: the SDK's own item - # classifiers (agents.models.chatcmpl_converter, - # Converter.maybe_easy_input_message) match - # EasyInputMessageParam on the exact key set - # {"content", "role"} — adding "type": "message" here - # makes it match maybe_response_output_message instead - # (any dict with type="message"/role="assistant"), whose - # branch assumes content is a list of output_text/refusal - # parts and blows up on a plain string. This is a prior - # assistant turn being replayed as *input*, so the plain - # EasyInputMessageParam shape is exactly right — no id, - # no status, no content-part wrapping needed. } ) for tool_call in message.tool_calls or []: @@ -311,7 +302,7 @@ def translate_assistant_message( return items def translate_tool_message(self, message: ToolMessage) -> dict[str, Any]: - """Translate a tool result into a function_call_output item. + """Translate an AG-UI tool result into an SDK function-call output. Args: message: The AG-UI tool message. @@ -325,52 +316,21 @@ def translate_tool_message(self, message: ToolMessage) -> dict[str, Any]: "output": message.content or "", } - def translate_reasoning_message( - self, - message: ReasoningMessage, - ) -> dict[str, Any] | None: - """Translate a reasoning trace into a reasoning item, if replayable. - - OpenAI o-series models can re-ingest encrypted reasoning blobs - (the encrypted_value field) but treat plaintext reasoning as - opaque. A reasoning item is only emitted when an encrypted value - is present; otherwise the message is dropped with a debug log. - - Args: - message: The AG-UI reasoning message. - - Returns: - {"type": "reasoning", ...}, or None if not replayable. - """ - if not message.encrypted_value: - logger.debug( - "Dropping ReasoningMessage id=%s: no encrypted_value to re-ingest", - message.id, - ) - return None - return { - "type": "reasoning", - "id": message.id, - "encrypted_content": message.encrypted_value, - "summary": [{"type": "summary_text", "text": message.content or ""}], - } - def translate_activity_message( self, message: ActivityMessage, ) -> dict[str, Any] | None: - """Translate an activity status message — dropped by default. + """Drop an AG-UI activity message by default. - Activity messages describe UI side-effects (e.g. "user - navigated") that the model doesn't need to ingest. Subclasses - can override to fold them into the system prompt if a - particular use case needs it. + Neither Responses nor Chat Completions has an equivalent model-input + item. Subclasses may override this mapping for application-specific + activity that should be included in model input. Args: message: The AG-UI activity message. Returns: - Always None; no Responses-API equivalent exists. + Always ``None`` in the default implementation. """ logger.debug( "Dropping ActivityMessage id=%s activity_type=%s", @@ -380,15 +340,16 @@ def translate_activity_message( return None # ───────────────────────────────────────────────────────────────────── - # TIER 3b — Single tool + # LEVEL 3b — Client tool translation # ───────────────────────────────────────────────────────────────────── def translate_tool(self, tool: AGUITool) -> FunctionTool: - """Wrap one AG-UI Tool as an SDK FunctionTool proxy. + """Wrap a client-declared AG-UI tool as an SDK ``FunctionTool``. The proxy's invocation handler raises ClientToolPending so the outer run loop can pause the SDK and hand control back to the - client. + client. Client schemas remain non-strict because they are supplied + externally and may not follow the OpenAI strict-schema subset. Args: tool: The AG-UI client-declared tool. @@ -418,7 +379,7 @@ async def on_invoke_tool( ) # ───────────────────────────────────────────────────────────────────── - # TIER 3c — Content parts (multimodal) + # LEVEL 3c — Content-part translation (multimodal) # ───────────────────────────────────────────────────────────────────── def translate_content(self, content: Any) -> list[dict[str, Any]]: @@ -609,7 +570,7 @@ def translate_binary_content( return self._binary_as_file(part, mime) # ───────────────────────────────────────────────────────────────────── - # TIER 4 — Internal helpers + # LEVEL 4 — Internal helpers # ───────────────────────────────────────────────────────────────────── def _ensure_object_schema(self, parameters: Any) -> dict[str, Any]: diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index b69d39837d..b79fcb07a7 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -32,6 +32,7 @@ RunErrorEvent, RunFinishedEvent, RunStartedEvent, + ResumeEntry, StateSnapshotEvent, Tool, UserMessage, @@ -39,7 +40,10 @@ from ag_ui_openai_agents import AGUITranslator -def _run_input(with_tool: bool = False) -> RunAgentInput: +def _run_input( + with_tool: bool = False, + resume: list[ResumeEntry] | None = None, +) -> RunAgentInput: return RunAgentInput( thread_id="t1", run_id="r1", @@ -56,6 +60,7 @@ def _run_input(with_tool: bool = False) -> RunAgentInput: state={}, context=[], forwarded_props=None, + resume=resume, ) @@ -102,6 +107,7 @@ def test_to_openai_translates_messages_and_tools(): assert len(bundle.tools) == 1 assert isinstance(bundle.tools[0], FunctionTool) assert bundle.tools[0].name == "confirm" + assert bundle.tools[0].strict_json_schema is False def test_to_openai_without_tools_leaves_bundle_empty(): @@ -109,6 +115,13 @@ def test_to_openai_without_tools_leaves_bundle_empty(): assert bundle.tools == [] +def test_to_openai_preserves_resume_entries(): + resume = ResumeEntry(interrupt_id="interrupt-1", status="resolved", payload=True) + translated = AGUITranslator().to_openai(_run_input(resume=[resume])) + + assert translated.resume == [resume] + + # ── AGUITranslator.to_agui (streaming) ─────────────────────────────────── diff --git a/integrations/openai-agents/python/uv.lock b/integrations/openai-agents/python/uv.lock index 158b9526d7..563029bf3f 100644 --- a/integrations/openai-agents/python/uv.lock +++ b/integrations/openai-agents/python/uv.lock @@ -29,7 +29,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "ag-ui-protocol", specifier = ">=0.1.18" }, + { name = "ag-ui-protocol", specifier = ">=0.1.19" }, { name = "fastapi", specifier = ">=0.115.12" }, { name = "openai-agents", specifier = ">=0.8.4" }, { name = "pydantic", specifier = ">=2.13.4" }, @@ -390,7 +390,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ From 4e7a4baa18f40b6cd18850b6b39ac57556914fd1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 15 Jul 2026 12:14:00 +0200 Subject: [PATCH 57/94] docs(openai-agents): clarify input translation --- integrations/openai-agents/python/README.md | 33 +++++++++-------- .../engine/agui_to_openai.py | 35 +++++++++---------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 692765ebaf..6576461f2e 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -569,7 +569,7 @@ Both directions, source of truth is `engine/agui_to_openai.py` and | `ActivityMessage` | dropped (neither Responses nor Chat Completions has an equivalent model-input item; subclasses may override this mapping) | | `Tool` (client-declared) | SDK `FunctionTool` proxy that raises `ClientToolPending` when invoked | | Content parts: `TextInputContent`, `ImageInputContent`, `AudioInputContent`, `DocumentInputContent`, `BinaryInputContent` | `input_text` / `input_image` / `input_audio` / `input_file` parts | -| `VideoInputContent` | dropped (no OpenAI API accepts video input yet) | +| `VideoInputContent` | dropped (neither Responses nor Chat Completions accepts video input) | **Outbound** — SDK stream event → AG-UI `BaseEvent` (`OpenAIToAGUITranslator`): @@ -626,20 +626,25 @@ more than text — images, audio clips, documents, etc. Each one is a typed content part (`ag_ui.core.InputContent`). The translator converts each part into the block shape the OpenAI Agents SDK expects for that message: -| User sends this... | ...becomes this for the SDK | How | +| User sends this... | SDK shape | Transport support | |---|---|---| -| Plain text | `input_text` | as-is | -| An image | `input_image` | a URL is passed through; raw bytes become a base64 `data:` URL | -| An audio clip | `input_audio` | must be raw bytes (base64) — audio can't be sent as a URL; the audio format (`wav`, `mp3`, ...) is read from the clip's mime type | -| A document (PDF, etc.) | `input_file` | URL stays a URL; raw bytes become base64 | -| A video | *(dropped, see below)* | not supported — see below | -| `BinaryInputContent` (deprecated, legacy catch-all) | whichever of the above matches its mime type | e.g. `image/*` → `input_image` | - -**Video input isn't supported, and it's not our limitation to fix.** Right -now, no OpenAI API (Responses or Chat Completions) accepts video as input at -all — there's no video content type to translate into. If OpenAI adds one, -we'll wire it up. Until then, video parts are silently dropped (logged, not -an error). If you want to work around it today, override +| Plain text | `input_text` | Responses and Chat Completions | +| An image | `input_image`; URL passes through and inline data becomes a base64 `data:` URL | Responses and Chat Completions | +| An audio clip | `input_audio` with base64 data and a format tag | Chat Completions with an audio-capable model; not Responses | +| A document (PDF, etc.) | `input_file`; URL uses `file_url` and inline data uses base64 `file_data` | Inline data works with both; URL works with Responses only | +| A video | *(dropped, see below)* | Neither Responses nor Chat Completions | +| `BinaryInputContent` (deprecated, legacy catch-all) | selected from its mime type, e.g. `image/*` becomes `input_image` | Depends on the selected content type | + +Base64 represents binary bytes as text that can be embedded safely in JSON. It +is encoding, not encryption or compression, and adds roughly 33% size overhead. +AG-UI data sources already contain base64: the translator wraps image and file +data in `data:;base64,...` URLs, while audio uses the base64 value with a +separate format tag. + +**Video isn't supported as an agent model input.** Neither Responses nor Chat +Completions has a video content part to translate into. OpenAI's specialized +Videos API is separate from these agent inputs. Until support is added, video +parts are silently dropped (logged, not an error). To work around it, override `translate_video_content` in a subclass — e.g. extract a few frames and send them as images instead. diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index 4613515321..3f22d63c7b 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -428,8 +428,7 @@ def translate_content_part(self, part: Any) -> dict[str, Any] | None: return self.translate_document_content(part) if isinstance(part, BinaryInputContent): return self.translate_binary_content(part) - # Not a typed part — probably a raw dict from loosely-parsed JSON. - # Fall back to sniffing it by shape. + # Support dict-shaped parts passed directly to the mapping API. return self._dispatch_dict_content_part(part) def translate_text_content(self, part: TextInputContent) -> dict[str, Any]: @@ -471,10 +470,9 @@ def translate_audio_content( ) -> dict[str, Any] | None: """Translate an AudioInputContent part. - The Responses API accepts base64 audio data with a short format - tag (wav, mp3). The format is extracted from the mime type. URL - sources are not supported by the API for audio — they get - dropped. + OpenAI accepts base64 audio through Chat Completions with an + audio-capable model; Responses does not accept audio input. URL audio + sources are unsupported and are dropped. Args: part: The AG-UI audio content part. @@ -503,8 +501,8 @@ def translate_video_content( ) -> dict[str, Any] | None: """Translate a VideoInputContent part — dropped by default. - The Responses API has no native video input. Override this in - a subclass to swap in a placeholder or extract frames. + Neither Responses nor Chat Completions has a native video input part. + Override this in a subclass to use a placeholder or extract frames. Args: part: The AG-UI video content part. @@ -512,7 +510,7 @@ def translate_video_content( Returns: Always None. """ - logger.debug("Dropping video part: Responses API does not accept video input") + logger.debug("Dropping video part: OpenAI model inputs do not accept video") return None def translate_document_content( @@ -547,12 +545,11 @@ def translate_binary_content( self, part: BinaryInputContent, ) -> dict[str, Any] | None: - """Translate a BinaryInputContent part, routed by mime type. + """Translate the deprecated BinaryInputContent catch-all by mime type. - Binary parts are a polymorphic catch-all: they may be images, - audio, documents, etc. The mime type is sniffed and routed to - the corresponding Responses-API input shape. Unknown types are - dropped. + This legacy AG-UI type predates dedicated image, audio, video, and + document parts. Image and audio mime types use their matching SDK + shapes; all remaining mime types are represented as files. Args: part: The AG-UI binary content part. @@ -573,7 +570,8 @@ def translate_binary_content( # LEVEL 4 — Internal helpers # ───────────────────────────────────────────────────────────────────── - def _ensure_object_schema(self, parameters: Any) -> dict[str, Any]: + @staticmethod + def _ensure_object_schema(parameters: Any) -> dict[str, Any]: """Normalize a possibly-empty tool parameter spec into a JSON Schema object. The Responses API requires every function tool's schema to be a @@ -593,10 +591,9 @@ def _ensure_object_schema(self, parameters: Any) -> dict[str, Any]: @staticmethod def _data_source_to_url(source: Any) -> str | None: - """Render a content source (data or url) as a single string for image_url. + """Render an image source as the URL string used by the Agents SDK. - URL sources pass through. Data sources become - data:;base64,. + Both SDK transports accept URL sources and base64 data URLs. Args: source: The content part's source object. @@ -619,7 +616,7 @@ def _data_source_to_url(source: Any) -> str | None: @staticmethod def _audio_format_from_mime(mime: str | None) -> str: - """Map a mime type to the short format string the Responses API wants. + """Map a mime type to the format expected for Chat Completions audio. Args: mime: The audio mime type, or None. From 31c5cad206d53d2ad47d3ed3a8babd9e3b465ec7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 15 Jul 2026 18:44:41 +0200 Subject: [PATCH 58/94] refactor(openai-agents): enhance OpenAIToAGUITranslator documentation and streamline state management --- integrations/openai-agents/python/README.md | 43 ++++++- .../engine/openai_to_agui.py | 110 ++++++++---------- 2 files changed, 86 insertions(+), 67 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 6576461f2e..94b09cde3f 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -741,10 +741,45 @@ Unknown SDK event types are skipped with a debug log instead of crashing the run The public translator delegates to two independent, symmetric engine translators in `ag_ui_openai_agents.engine`: -- `AGUIToOpenAITranslator` — inbound; stateless, tiered per-type methods - (`translate_user_message`, `translate_tool_message`, ...) -- `OpenAIToAGUITranslator` — outbound; stateful per run (open text/tool/reasoning - windows), per-type methods (`translate_text_delta`, `translate_item`, ...) +- `AGUIToOpenAITranslator` — inbound and stateless. Each request is converted + independently, so one instance can be reused. +- `OpenAIToAGUITranslator` — outbound and stateful per run. OpenAI Agents SDK + events arrive incrementally, so it remembers open text, tool-call, reasoning, + and agent-step sequences between events. + +`AGUITranslator` remains stateless and reusable. It reuses the inbound engine +and creates a fresh outbound engine for every `to_agui()` call. + +The public translator emits events in this order: + +```text +RUN_STARTED + → optional start event and initial state + → streamed STEP / REASONING / TEXT / TOOL sequences + → finalize open sequences + → optional final state and MESSAGES_SNAPSHOT + → optional end event + → RUN_FINISHED +``` + +Within the outbound engine, each content sequence is correlated across OpenAI +Agents SDK events and emitted as `START → content/arguments → END`. + +### Outbound state model + +An internal correlation key joins events for the same OpenAI Agents SDK output +item. It uses the real item ID when available and otherwise uses the item's +output position. The value stored for an open sequence is the ID already sent +to the AG-UI client, ensuring later content and closing events reuse it. + +| State group | Attributes | Why it is retained | +|---|---|---| +| Text | `_open_texts`, `_pending_text_ids`, `_closed_text_ids` | Reuse message IDs, avoid empty assistant messages on tool-only turns, and prevent completed run items from duplicating streamed text. | +| Tool calls | `_open_tool_calls`, `_seen_call_ids` | Attach streamed arguments to the correct tool call and prevent raw events and completed tool items from emitting the same call twice. | +| Reasoning | `_open_reasonings`, `_open_reasoning_parts`, `_closed_reasoning_ids` | Preserve phase and part ordering, then reconcile completed reasoning items without duplicate output. | +| Reasoning metadata | `_reasoning_part_seq`, `_reasoning_phase_ids`, `_emitted_encrypted_keys` | Give multiple reasoning parts stable IDs and attach encrypted replay data once, even when it arrives after visible reasoning closes. | +| Agent steps | `_current_step` | Finish the previous agent step before starting the next and close the final step when the stream ends. | +| Message snapshot | `_snapshot_messages` | Build `MESSAGES_SNAPSHOT` with the same IDs used by streamed text, tool calls, and tool results. | Every per-type method is a public override point. To customize one mapping, subclass the engine and inject it — the public translator and every other mapping stay diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index de4634feb5..339662cd3b 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -1,14 +1,4 @@ -"""Outbound translator: OpenAI Agents SDK events → AG-UI BaseEvent. - -Same layered shape as AGUIToOpenAITranslator, just the other direction. -Stateful per run — it tracks open text / tool-call / reasoning windows so -AG-UI always sees strict START → CONTENT → END triplets. Make a fresh one -per run; don't share across runs. - -Run-envelope events (RUN_STARTED / RUN_FINISHED / RUN_ERROR, -STATE_SNAPSHOT / STATE_DELTA, MESSAGES_SNAPSHOT) are the run loop's job -— the translator only translates. -""" +"""Translate OpenAI Agents SDK stream events into AG-UI events.""" import logging from typing import Any, Sequence @@ -74,71 +64,65 @@ class OpenAIToAGUITranslator: - """Translate OpenAI Agents SDK outputs into AG-UI BaseEvent objects. - - Wire ids are reused, never invented — with one caveat: some model - backends stamp every item with the SDK's FAKE_RESPONSES_ID - placeholder instead of a real id. Windows are therefore keyed by - real item id when available and by the raw event's output_index - otherwise, and placeholder ids are replaced with generated ones so - AG-UI clients never see two different messages sharing an id. - - The pattern is "lazy open, eager close": a *_START is emitted when - the first signal for a window arrives (output_item.added or, - defensively, a bare delta), and *_END fires on the first close - signal — output_item.done, the run-item commit, or finalize(), - whichever comes first. Run items double as a full emission path: - when no window was ever opened for an item (some backends never - stream raw deltas for it), its run-item translator emits the - complete triplet from the finished item. + """Translate one OpenAI Agents SDK run into AG-UI events. + + This translator is stateful and must be created once per run. It tracks + open text, tool-call, reasoning, and agent-step sequences so each AG-UI + sequence follows START, content, and END ordering. + + Raw response events provide incremental content. Completed run items close + streamed sequences or emit a complete sequence when no deltas arrived. + ``finalize()`` closes any sequence still open when the stream ends. + + OpenAI Agents SDK item IDs are reused when available. Missing or placeholder + IDs are replaced with generated AG-UI IDs, while internal correlation keys + ensure later deltas and completion events update the same sequence. + + ``AGUITranslator`` owns run-level lifecycle and snapshot events, including + ``RUN_STARTED``, ``RUN_FINISHED``, ``RUN_ERROR``, and message/state snapshots. """ def __init__(self) -> None: - # What's currently open. Key is the real item id when we have one, - # otherwise __idx_. Value is the id we already sent in - # the matching *_START event, so we can pair the END to it. - self._open_texts: dict[str, str] = {} # key -> message_id - # A message item that was announced (output_item.added) but whose - # TEXT_MESSAGE_START we're holding back until a real delta arrives. - # Value is the id the START will carry once (if) it opens. Lets a - # text-less item — a pure tool-call turn on some providers — pass - # through without an empty text window bracketing the tool call. - self._pending_text_ids: dict[str, str] = {} # key -> deferred message_id - self._open_tool_calls: dict[str, str] = {} # key -> tool_call_id - self._open_reasonings: dict[str, str] = {} # key -> phase message_id + # Text sequences: defer empty messages and reconcile streamed commits. + self._open_texts: dict[str, str] = {} # key -> message_id + self._pending_text_ids: dict[str, str] = {} # key -> deferred message_id + self._closed_text_ids: list[str] = [] # closed message_ids + + # Tool-call sequences: keep streamed arguments and commits deduplicated. + self._open_tool_calls: dict[str, str] = {} # key -> tool_call_id + self._seen_call_ids: set[str] = set() # emitted tool_call_ids + + # Reasoning sequences: track phases, parts, replay data, and reconciliation. + self._open_reasonings: dict[str, str] = {} # key -> phase message_id self._open_reasoning_parts: dict[str, str] = {} # key -> part message_id - # When a run item shows up, we need to know if we already streamed and - # closed it (nothing to do) or if no deltas ever arrived for it and we - # have to emit it whole. These track what's been closed so we can tell. - self._closed_text_ids: list[str] = [] - self._closed_reasoning_ids: list[str] = [] - self._seen_call_ids: set[str] = set() - self._emitted_encrypted_keys: set[str] = set() - self._reasoning_part_seq: dict[str, int] = {} - # Never cleared: an encrypted value can land after we've already closed - # the reasoning phase, and we still need the original id to attach it to. - self._reasoning_phase_ids: dict[str, str] = {} - self._current_step: str | None = None - # AG-UI Messages for the end-of-run MESSAGES_SNAPSHOT, built inline as - # each item resolves its id here — never a second pass over new_items, - # so a snapshot message's id can never diverge from its streamed one. - # Reasoning items are intentionally never appended (see - # translate_reasoning_item). - self._snapshot_messages: list[Message] = [] + self._closed_reasoning_ids: list[str] = [] # closed phase message_ids + self._emitted_encrypted_keys: set[str] = set() # emitted keys + self._reasoning_part_seq: dict[str, int] = {} # key -> next part index + self._reasoning_phase_ids: dict[str, str] = {} # key -> phase message_id + + # Agent-step state for ordered STEP_FINISHED and STEP_STARTED events. + self._current_step: str | None = None # active step name + + # Completed messages using the same IDs as their streamed events. + self._snapshot_messages: list[Message] = [] # completed AG-UI messages # ───────────────────────────────────────────────────────────────────── # TIER 1 — One-shot entry points # ───────────────────────────────────────────────────────────────────── def translate(self, sdk_event: Any) -> list[BaseEvent]: - """Dispatch one SDK StreamEvent to the right family translator. + """Translate one OpenAI Agents SDK stream event into ordered AG-UI events. + + Raw response events stream START, content, and END sequences. Run-item + events close those sequences or emit them whole when no deltas arrived. + Agent updates emit STEP_FINISHED before STEP_STARTED. - Returns a list because one SDK event can produce several AG-UI - events (e.g. an output_item.added that force-opens a window). - Unknown event types translate to [] with a debug log. + Across a run, ``AGUITranslator.to_agui()`` emits RUN_STARTED, optional + initial events, translated stream sequences, finalized closing events, + optional snapshots, and RUN_FINISHED. It emits RUN_ERROR on failure. Args: - sdk_event: One event from result.stream_events(). + sdk_event: One event from ``result.stream_events()``. Returns: Zero or more translated AG-UI events. From 24229aa4c89f208108654a67c86d03972ca71884 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 15 Jul 2026 18:49:16 +0200 Subject: [PATCH 59/94] rename(openai-agents): sdk_event -> openai_event across outbound translator --- .../src/ag_ui_openai_agents/engine/openai_to_agui.py | 12 ++++++------ .../python/src/ag_ui_openai_agents/translator.py | 4 ++-- .../openai-agents/python/tests/test_translators.py | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index 339662cd3b..880e45a943 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -110,7 +110,7 @@ def __init__(self) -> None: # TIER 1 — One-shot entry points # ───────────────────────────────────────────────────────────────────── - def translate(self, sdk_event: Any) -> list[BaseEvent]: + def translate(self, openai_event: Any) -> list[BaseEvent]: """Translate one OpenAI Agents SDK stream event into ordered AG-UI events. Raw response events stream START, content, and END sequences. Run-item @@ -122,18 +122,18 @@ def translate(self, sdk_event: Any) -> list[BaseEvent]: optional snapshots, and RUN_FINISHED. It emits RUN_ERROR on failure. Args: - sdk_event: One event from ``result.stream_events()``. + openai_event: One event from ``result.stream_events()``. Returns: Zero or more translated AG-UI events. """ - event_type = read_attr(sdk_event, "type") + event_type = read_attr(openai_event, "type") if event_type == OpenAIStreamEventType.RAW_RESPONSE: - return self.translate_raw_response_event(sdk_event) + return self.translate_raw_response_event(openai_event) if event_type == OpenAIStreamEventType.RUN_ITEM: - return self.translate_run_item_event(sdk_event) + return self.translate_run_item_event(openai_event) if event_type == OpenAIStreamEventType.AGENT_UPDATED: - return self.translate_agent_updated_event(sdk_event) + return self.translate_agent_updated_event(openai_event) logger.debug("Unknown SDK stream event type: %s", event_type) return [] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index aadd17117e..cfc0c94d22 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -153,8 +153,8 @@ async def to_agui( outbound = self._outbound_cls() try: # Streamed STEP / TEXT / TOOL / REASONING events. - async for sdk_event in stream_events: - for event in outbound.translate(sdk_event): + async for openai_event in stream_events: + for event in outbound.translate(openai_event): yield event # 4. Close any text, tool, reasoning, or step window still open. for event in outbound.finalize(): diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translators.py index b79fcb07a7..d3676d9901 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translators.py @@ -77,8 +77,8 @@ def __init__(self) -> None: type(self).instances += 1 self._snapshot_messages: list = [] - def translate(self, sdk_event): - return [_event(f"translated:{sdk_event}")] + def translate(self, openai_event): + return [_event(f"translated:{openai_event}")] def finalize(self): return [_event("finalized")] @@ -259,7 +259,7 @@ async def collect(): def test_to_agui_emits_run_error_and_reraises_on_exception(): class _ExplodingOutbound(_StubOutbound): - def translate(self, sdk_event): + def translate(self, openai_event): raise RuntimeError("boom") translator = AGUITranslator(outbound_cls=_ExplodingOutbound) @@ -279,7 +279,7 @@ async def collect(): def test_to_agui_emit_run_error_false_suppresses_event_but_still_raises(): class _ExplodingOutbound(_StubOutbound): - def translate(self, sdk_event): + def translate(self, openai_event): raise RuntimeError("boom") translator = AGUITranslator(outbound_cls=_ExplodingOutbound) @@ -300,7 +300,7 @@ async def collect(): def test_to_agui_run_error_message_overrides_exception_text(): class _ExplodingOutbound(_StubOutbound): - def translate(self, sdk_event): + def translate(self, openai_event): raise RuntimeError("raw internal detail") translator = AGUITranslator(outbound_cls=_ExplodingOutbound) @@ -325,7 +325,7 @@ def test_to_agui_emits_run_error_on_cancelled_error(): # mid-stream timeout/dropped-connection must still surface RUN_ERROR # instead of silently ending the stream after whatever was last yielded. class _CancelledOutbound(_StubOutbound): - def translate(self, sdk_event): + def translate(self, openai_event): raise asyncio.CancelledError() translator = AGUITranslator(outbound_cls=_CancelledOutbound) From dcc04312f5a2b23bd119c1c3e24db4f28861fd4b Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 15 Jul 2026 21:27:20 +0200 Subject: [PATCH 60/94] docs(openai-agents): update comments and documentation for clarity and consistency --- .../engine/openai_to_agui.py | 119 ++++++++---------- 1 file changed, 54 insertions(+), 65 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index 880e45a943..e105d80493 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -107,7 +107,7 @@ def __init__(self) -> None: self._snapshot_messages: list[Message] = [] # completed AG-UI messages # ───────────────────────────────────────────────────────────────────── - # TIER 1 — One-shot entry points + # LEVEL 1 — Stream lifecycle # ───────────────────────────────────────────────────────────────────── def translate(self, openai_event: Any) -> list[BaseEvent]: @@ -125,7 +125,7 @@ def translate(self, openai_event: Any) -> list[BaseEvent]: openai_event: One event from ``result.stream_events()``. Returns: - Zero or more translated AG-UI events. + Ordered AG-UI events, or an empty list if nothing is translatable. """ event_type = read_attr(openai_event, "type") if event_type == OpenAIStreamEventType.RAW_RESPONSE: @@ -134,17 +134,17 @@ def translate(self, openai_event: Any) -> list[BaseEvent]: return self.translate_run_item_event(openai_event) if event_type == OpenAIStreamEventType.AGENT_UPDATED: return self.translate_agent_updated_event(openai_event) - logger.debug("Unknown SDK stream event type: %s", event_type) + logger.debug("Unknown OpenAI Agents SDK stream event type: %s", event_type) return [] def finalize(self) -> list[BaseEvent]: - """Emit pending *_END markers when the SDK stream terminates. + """Emit pending *_END markers when the OpenAI Agents SDK stream terminates. Always call this after the stream ends — even on error — so the AG-UI client never sees a window that opened but never closed. Returns: - Closing events for any windows still open. + Closing events, or an empty list if no windows or agent step remain open. """ events: list[BaseEvent] = [] for key in list(self._open_texts): @@ -169,29 +169,26 @@ def build_messages_snapshot( ) -> MessagesSnapshotEvent: """Build the end-of-run MESSAGES_SNAPSHOT event. - The snapshot is the full conversation as the client should know - it: the run's prior messages passed through untouched (they keep - the ids the client already renders, so its id-keyed merge updates - in place instead of duplicating), followed by this run's messages - — collected here as the stream ran (see _record_* below), each - under the exact id its streamed event used. Same id, one - resolution: a snapshot message can never disagree with its - streamed counterpart, even when a backend emits placeholder ids - instead of real ones. - - Prior history comes from run_input.messages, not - result.to_input_list(): Responses-API input items carry no id slot - for user/system messages, so round-tripping prior turns through - them would mint fresh ids and duplicate every bubble on the - client. Filter run_input first if it carries anything the client - should not see echoed back (e.g. a system prompt sent as history). + The snapshot contains the prior AG-UI message history followed by + messages completed during this run. Prior messages retain their AG-UI + IDs, and completed messages reuse the IDs emitted by their streaming + events, allowing clients to merge the snapshot without duplicates. + + Prior history comes from ``run_input.messages``, not + ``result.to_input_list()``. The OpenAI Agents SDK represents model input + as Responses-shaped items even when the underlying model uses Chat + Completions. Round-tripping prior AG-UI messages through that format + does not preserve their AG-UI IDs. + + Callers should remove messages that must not be echoed to the client, + such as server-only system instructions, before passing ``run_input``. Args: - run_input: The run's RunAgentInput, a plain message list, or - None when there is no prior history to prepend. + run_input: The original run input, a message sequence, or ``None`` + when there is no prior history. Returns: - A MESSAGES_SNAPSHOT event ready to encode. + A ``MESSAGES_SNAPSHOT`` containing prior and completed messages. """ if run_input is None: prior: list[Message] = [] @@ -205,17 +202,17 @@ def build_messages_snapshot( ) # ───────────────────────────────────────────────────────────────────── - # TIER 2 — Bulk collection + per event family + # LEVEL 2 — Event-family translation # ───────────────────────────────────────────────────────────────────── def translate_raw_response_event(self, event: Any) -> list[BaseEvent]: """Handle a RawResponsesStreamEvent (token-level Responses deltas). Args: - event: The SDK's RawResponsesStreamEvent. + event: The OpenAI Agents SDK's RawResponsesStreamEvent. Returns: - Zero or more translated AG-UI events. + Translated AG-UI events, or an empty list if ignored or invalid. """ data = read_attr(event, "data") if data is None: @@ -251,10 +248,10 @@ def translate_run_item_event(self, event: Any) -> list[BaseEvent]: """Handle a RunItemStreamEvent (semantic commit signals). Args: - event: The SDK's RunItemStreamEvent. + event: The OpenAI Agents SDK's RunItemStreamEvent. Returns: - Zero or more translated AG-UI events. + Translated AG-UI events, or an empty list if the event has no output. """ item = read_attr(event, "item") if item is None: @@ -268,7 +265,7 @@ def translate_agent_updated_event(self, event: Any) -> list[BaseEvent]: surfaced as an AG-UI step named after the agent. Args: - event: The SDK's AgentUpdatedStreamEvent. + event: The OpenAI Agents SDK's AgentUpdatedStreamEvent. Returns: STEP_FINISHED for the previous step (if any) followed by @@ -291,16 +288,14 @@ def translate_agent_updated_event(self, event: Any) -> list[BaseEvent]: return events # ───────────────────────────────────────────────────────────────────── - # TIER 3a — Raw Responses deltas (per raw type) + # LEVEL 3a — Raw Responses event translation # ───────────────────────────────────────────────────────────────────── def translate_output_item_added(self, data: Any) -> list[BaseEvent]: - """Translate response.output_item.added by opening the matching AG-UI window. + """Prepare the AG-UI sequence for a new Responses output item. - message opens TEXT_MESSAGE_START, function_call opens - TOOL_CALL_START, reasoning opens REASONING_START, and hosted - tool calls open TOOL_CALL_START with the tool name set to the - item type. + Messages defer their text sequence until content arrives. Function and + hosted tool calls open tool-call sequences; reasoning opens its sequence. Args: data: The raw output_item.added payload. @@ -314,14 +309,8 @@ def translate_output_item_added(self, data: Any) -> list[BaseEvent]: key = self._window_key(item_id, read_attr(data, "output_index")) if item_type == OpenAIItemType.MESSAGE: - # Defer TEXT_MESSAGE_START until a real delta actually arrives. - # Some backends emit a message item even on a turn that ends up - # being a pure tool call with no spoken text — opening the window - # here would leave an empty TEXT_MESSAGE_START/END pair wrapping - # the tool call on the wire. Remember the real id so the lazy open still - # uses it. Output has begun, though, so close any open reasoning - # now (some backends send reasoning-done late) — that must not - # wait for the deferred text. + # Defer text until its first delta to avoid empty messages on tool-only + # turns. Close reasoning now because some providers send its done late. self._pending_text_ids[key] = self._resolve_id(item_id, new_message_id) return self._close_all_reasonings() if item_type == OpenAIItemType.FUNCTION_CALL: @@ -353,10 +342,8 @@ def translate_output_item_done(self, data: Any) -> list[BaseEvent]: key = self._window_key(read_attr(item, "id"), read_attr(data, "output_index")) if item_type == OpenAIItemType.MESSAGE: - # Drop a deferred-but-never-opened window: no delta ever arrived, - # so there's nothing on the wire to close and no empty window to - # emit. If a delta did arrive the pending entry is already gone - # and this pop is a no-op; _close_text then closes the real one. + # Discard a pending message if no text delta opened it; otherwise + # _close_text closes the active sequence below. self._pending_text_ids.pop(key, None) return self._close_text(key) if item_type == OpenAIItemType.FUNCTION_CALL or item_type in HOSTED_TOOL_CALL_TYPES: @@ -487,21 +474,21 @@ def translate_reasoning_part_done(self, data: Any) -> list[BaseEvent]: return self._close_reasoning_part(key) # ───────────────────────────────────────────────────────────────────── - # TIER 3b — Single run item (dispatcher + per-type) + # LEVEL 3b — Run-item translation # ───────────────────────────────────────────────────────────────────── def translate_item(self, item: RunItem) -> list[BaseEvent]: - """Dispatch one SDK RunItem to the right per-type translator. + """Dispatch one OpenAI Agents SDK RunItem to its per-type translator. Usually these act as safety-net closers (raw events already streamed the content); when no deltas were ever streamed for the item, they emit its full AG-UI sequence instead. Args: - item: A finished SDK RunItem. + item: A finished OpenAI Agents SDK RunItem. Returns: - Zero or more translated AG-UI events. + Translated AG-UI events, or an empty list if the item has no output. """ if isinstance(item, MessageOutputItem): return self.translate_message_output_item(item) @@ -521,7 +508,7 @@ def translate_item(self, item: RunItem) -> list[BaseEvent]: return self.translate_mcp_list_tools_item(item) if isinstance(item, MCPApprovalResponseItem): return self.translate_mcp_approval_response_item(item) - logger.warning("Unknown SDK run item type: %s", type(item).__name__) + logger.warning("Unknown OpenAI Agents SDK run item type: %s", type(item).__name__) return [] def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEvent]: @@ -529,7 +516,7 @@ def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEve Streamed: the raw deltas already carried the text — just close. No deltas seen: emit TEXT_MESSAGE_START/CONTENT/END from the - finished item, using the SDK's own ItemHelpers extractors (text + finished item, using the OpenAI Agents SDK's ItemHelpers extractors (text first, refusal as fallback — both are user-visible). Args: @@ -579,7 +566,7 @@ def translate_tool_call_item(self, item: ToolCallItem) -> list[BaseEvent]: """Handle a tool call commit by closing its window or emitting START/[ARGS]/END. Reconciled by call_id (present and real regardless of backend), - using the SDK's built-in ToolCallItem.call_id / .tool_name + using the OpenAI Agents SDK's built-in ToolCallItem.call_id / .tool_name properties where available — getattr fallbacks because HandoffCallItem routes through here too and lacks them. @@ -787,16 +774,17 @@ def translate_mcp_approval_response_item( return [] # ───────────────────────────────────────────────────────────────────── - # TIER 4 — Internal helpers: id handling + # LEVEL 4 — Internal helpers: id handling # ───────────────────────────────────────────────────────────────────── @staticmethod def _is_real_id(item_id: Any) -> bool: """Check whether an id is usable on the wire (not empty, not a placeholder). - Some backends stamp every item with the SDK's FAKE_RESPONSES_ID + Some backends stamp every item with the OpenAI Agents SDK's + FAKE_RESPONSES_ID sentinel — sharing it across AG-UI events would collide every - message of the run. Checked against the SDK's own constant, so + message of the run. Checked against the OpenAI Agents SDK's constant, so it is a no-op on native OpenAI and correct for any backend that does not use placeholder ids. @@ -823,18 +811,19 @@ def _resolve_id(cls, item_id: Any, generate: Any) -> str: @classmethod def _window_key(cls, item_id: Any, output_index: Any) -> str: - """Compute a stable window key for correlating raw events of one output item. + """Return the internal key that correlates one output item's raw events. - Real item ids win; placeholder ids fall back to the stream - position (output_index), which the Responses API guarantees is - unique per item within a response. + This key indexes open text, tool-call, and reasoning sequences; it is + never emitted as an AG-UI ID. A real OpenAI item ID is used when + available. Missing or placeholder IDs fall back to ``output_index``, + which is unique per item within a response. Args: item_id: The item's wire id, or None. output_index: The item's position in the response. Returns: - A stable key for this item's window. + The OpenAI item ID, or ``__idx_`` as a fallback. """ if cls._is_real_id(item_id): return item_id @@ -881,7 +870,7 @@ def _reconcile( return ("new", None) # ───────────────────────────────────────────────────────────────────── - # TIER 4 — Internal helpers: window management + # LEVEL 4 — Internal helpers: window management # ───────────────────────────────────────────────────────────────────── def _open_text(self, key: str, message_id: str) -> list[BaseEvent]: @@ -1047,7 +1036,7 @@ def _emit_encrypted_value(self, key: str, item: Any) -> list[BaseEvent]: ] # ───────────────────────────────────────────────────────────────────── - # TIER 4 — Internal helpers: snapshot message builders + # LEVEL 4 — Internal helpers: snapshot message builders # ───────────────────────────────────────────────────────────────────── # # One per streamed item, appended as the item commits — always with the From d2c35e041528466bce92eb6b4ef1aeb52ba266de Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 15 Jul 2026 23:17:33 +0200 Subject: [PATCH 61/94] docs(openai-agents): clarify outbound helpers --- .../engine/openai_to_agui.py | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index e105d80493..eaeb76fe8b 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -774,7 +774,7 @@ def translate_mcp_approval_response_item( return [] # ───────────────────────────────────────────────────────────────────── - # LEVEL 4 — Internal helpers: id handling + # LEVEL 4a — ID handling # ───────────────────────────────────────────────────────────────────── @staticmethod @@ -847,14 +847,10 @@ def _reconcile( closed_ids: Ids already closed via streaming, oldest first. Returns: - ("close", key) when a streamed window is still open, - ("skip", resolved_id) when the item was already fully - streamed and closed — resolved_id is the id that streamed - close already used, so the caller can still record a - snapshot message under it (real id: raw_id itself, since - it's known directly; placeholder id: the queued closed id), - or ("new", None) when nothing was streamed for the item and - it must be emitted whole. + A tuple[str, str | None] containing one of: + - ("close", key): Close the matching open sequence. + - ("skip", resolved_id): Reuse an already closed sequence's ID. + - ("new", None): Emit the complete item; no sequence exists. """ if self._is_real_id(raw_id): if raw_id in open_windows: @@ -870,15 +866,14 @@ def _reconcile( return ("new", None) # ───────────────────────────────────────────────────────────────────── - # LEVEL 4 — Internal helpers: window management + # LEVEL 4b — Sequence management # ───────────────────────────────────────────────────────────────────── def _open_text(self, key: str, message_id: str) -> list[BaseEvent]: + """Open a text sequence, closing active reasoning first.""" if key in self._open_texts: return [] - # Once real output starts, the model is done thinking. Close any open - # reasoning now instead of waiting for the done-event — some backends - # send it late, and we don't want reasoning bleeding into the answer. + # Close reasoning before assistant text; its done event may arrive late. events = self._close_all_reasonings() self._open_texts[key] = message_id events.append( @@ -891,13 +886,12 @@ def _open_text(self, key: str, message_id: str) -> list[BaseEvent]: return events def _emit_text_content(self, key: str, delta: str) -> list[BaseEvent]: + """Emit text content, opening its sequence when needed.""" if not delta: return [] events: list[BaseEvent] = [] if key not in self._open_texts: - # First real delta for a deferred item opens the window now, under - # the id output_item.added reserved (falling back to a fresh id if - # the deltas arrived with no added first). + # Open deferred text on its first delta; generate an ID if added is absent. events.extend( self._open_text(key, self._pending_text_ids.pop(key, None) or new_message_id()) ) @@ -911,6 +905,7 @@ def _emit_text_content(self, key: str, delta: str) -> list[BaseEvent]: return events def _close_text(self, key: str) -> list[BaseEvent]: + """Close a text sequence and remember its message ID.""" message_id = self._open_texts.pop(key, None) if message_id is None: return [] @@ -923,6 +918,7 @@ def _close_text(self, key: str) -> list[BaseEvent]: ] def _open_tool_call(self, key: str, call_id: str, name: str) -> list[BaseEvent]: + """Open a tool-call sequence, closing active reasoning first.""" if key in self._open_tool_calls: return [] events = self._close_all_reasonings() # see _open_text @@ -938,6 +934,7 @@ def _open_tool_call(self, key: str, call_id: str, name: str) -> list[BaseEvent]: return events def _close_tool_call(self, key: str) -> list[BaseEvent]: + """Close an active tool-call sequence.""" call_id = self._open_tool_calls.pop(key, None) if call_id is None: return [] @@ -949,6 +946,7 @@ def _close_tool_call(self, key: str) -> list[BaseEvent]: ] def _open_reasoning(self, key: str, phase_id: str) -> list[BaseEvent]: + """Open a reasoning sequence.""" if key in self._open_reasonings: return [] self._open_reasonings[key] = phase_id @@ -961,6 +959,7 @@ def _open_reasoning(self, key: str, phase_id: str) -> list[BaseEvent]: ] def _open_reasoning_part(self, key: str) -> list[BaseEvent]: + """Open the next part of a reasoning sequence.""" if key in self._open_reasoning_parts: return [] seq = self._reasoning_part_seq.get(key, 0) @@ -980,6 +979,7 @@ def _open_reasoning_part(self, key: str) -> list[BaseEvent]: ] def _close_reasoning_part(self, key: str) -> list[BaseEvent]: + """Close an active reasoning part.""" message_id = self._open_reasoning_parts.pop(key, None) if message_id is None: return [] @@ -991,6 +991,7 @@ def _close_reasoning_part(self, key: str) -> list[BaseEvent]: ] def _close_reasoning(self, key: str) -> list[BaseEvent]: + """Close a reasoning part and its parent sequence.""" phase_id = self._open_reasonings.pop(key, None) if phase_id is None: return [] @@ -1005,6 +1006,7 @@ def _close_reasoning(self, key: str) -> list[BaseEvent]: return events def _close_all_reasonings(self) -> list[BaseEvent]: + """Close every active reasoning sequence.""" events: list[BaseEvent] = [] for key in list(self._open_reasonings): events.extend(self._close_reasoning(key)) @@ -1036,21 +1038,19 @@ def _emit_encrypted_value(self, key: str, item: Any) -> list[BaseEvent]: ] # ───────────────────────────────────────────────────────────────────── - # LEVEL 4 — Internal helpers: snapshot message builders + # LEVEL 4c — Snapshot message builders # ───────────────────────────────────────────────────────────────────── - # - # One per streamed item, appended as the item commits — always with the - # id its streamed event already used, so build_messages_snapshot's output - # lines up with the stream. Reasoning is deliberately not recorded: the - # streamed reasoning bubbles survive the client merge on their own, and - # plaintext reasoning cannot be round-tripped back into an OpenAI run. + # Snapshot messages reuse streamed IDs so clients can merge without duplicates. + # Reasoning is omitted because plaintext reasoning cannot be replayed. def _record_text(self, message_id: str, text: str) -> None: + """Add an assistant text message to the snapshot.""" self._snapshot_messages.append( AssistantMessage(id=message_id, role="assistant", content=text) ) def _record_tool_call(self, call_id: str, name: str, arguments: str) -> None: + """Add an assistant tool call to the snapshot.""" # The streamed TOOL_CALL_START carried no parent message id, so the # client keyed the bubble by the tool call id — mirror that here. self._snapshot_messages.append( @@ -1068,6 +1068,7 @@ def _record_tool_call(self, call_id: str, name: str, arguments: str) -> None: ) def _record_result(self, call_id: str, content: str) -> str: + """Add a tool result and return its derived message ID.""" # Returns the derived result id so the caller reuses the same value # for its TOOL_CALL_RESULT event — snapshot and stream stay in sync. result_id = new_tool_result_id(call_id) From ecc27f69c6fbe7c0e3a987b6753cd8cf981faebd Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Wed, 15 Jul 2026 23:58:17 +0200 Subject: [PATCH 62/94] test(openai-agents): refactor project test suites --- integrations/openai-agents/python/README.md | 4 +- .../openai-agents/python/examples/README.md | 19 ++- .../python/examples/pyproject.toml | 8 ++ .../tests/test_ag_ui_docs_copilot.py} | 2 +- .../openai-agents/python/examples/uv.lock | 116 ++++++++++++++++++ .../openai-agents/python/pyproject.toml | 3 + .../src/ag_ui_openai_agents/engine/types.py | 2 +- .../test_agui_to_openai_multimodal.py} | 0 .../test_openai_to_agui.py} | 4 +- .../test_openai_to_agui_snapshot.py} | 0 .../test_types_drift.py} | 0 ...test_translators.py => test_translator.py} | 3 +- 12 files changed, 151 insertions(+), 10 deletions(-) rename integrations/openai-agents/python/{tests/test_ag_ui_docs_copilot_example.py => examples/tests/test_ag_ui_docs_copilot.py} (96%) rename integrations/openai-agents/python/tests/{test_multimodal.py => engine/test_agui_to_openai_multimodal.py} (100%) rename integrations/openai-agents/python/tests/{test_engine_mapping.py => engine/test_openai_to_agui.py} (98%) rename integrations/openai-agents/python/tests/{test_snapshot.py => engine/test_openai_to_agui_snapshot.py} (100%) rename integrations/openai-agents/python/tests/{test_stream_types_drift.py => engine/test_types_drift.py} (100%) rename integrations/openai-agents/python/tests/{test_translators.py => test_translator.py} (99%) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 94b09cde3f..5ad7161be8 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -596,7 +596,7 @@ Both directions, source of truth is `engine/agui_to_openai.py` and | `initial_state` / `final_state`, if provided | `STATE_SNAPSHOT` | Unknown SDK event or item types translate to `[]` with a debug log — -graceful degradation, never a raise. See `tests/test_engine_mapping.py` for +graceful degradation, never a raise. See `tests/engine/test_openai_to_agui.py` for the streaming behavior pinned event-by-event. ### Guardrails @@ -932,7 +932,7 @@ uv sync # installs dev group (pytest) uv run pytest # run the full suite ``` -The suite includes a **drift guard** (`tests/test_stream_types_drift.py`): +The suite includes a **drift guard** (`tests/engine/test_types_drift.py`): this package hardcodes the wire `type` strings it dispatches on (in `engine/types.py`), and the guard asserts each one against the `Literal[...]` annotations of the installed `openai-agents` / `openai` diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 429f5f4a9f..4420adb970 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -21,15 +21,28 @@ library. ```bash cd examples -uv sync +uv sync --no-dev cp .env.example .env # fill in OPENAI_API_KEY -uv run python server.py +uv run --no-dev python server.py ``` Server runs on **http://localhost:8024** (the port the AG-UI Dojo expects; override with `PORT`). -## Testing +## Automated tests + +The examples have an independent test suite. From this directory, install the +development dependencies and run it with: + +```bash +uv sync +uv run pytest +``` + +Running `uv run pytest` from the main SDK directory tests only the SDK suite; +it does not collect these example tests. + +## Manual smoke test ```bash curl -N -X POST http://localhost:8024/agentic_chat/ \ diff --git a/integrations/openai-agents/python/examples/pyproject.toml b/integrations/openai-agents/python/examples/pyproject.toml index ddcf3c6d6c..87498c6c7c 100644 --- a/integrations/openai-agents/python/examples/pyproject.toml +++ b/integrations/openai-agents/python/examples/pyproject.toml @@ -12,6 +12,14 @@ dependencies = [ "uvicorn[standard]>=0.35.0", ] +[dependency-groups] +dev = [ + "pytest>=8.0", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] + [tool.uv.sources] ag-ui-openai-agents = { path = "../", editable = true } diff --git a/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py b/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py similarity index 96% rename from integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py rename to integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py index 2d096f1cb5..69684d38fc 100644 --- a/integrations/openai-agents/python/tests/test_ag_ui_docs_copilot_example.py +++ b/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py @@ -6,7 +6,7 @@ import sys from pathlib import Path -EXAMPLES = Path(__file__).resolve().parents[1] / "examples" +EXAMPLES = Path(__file__).resolve().parents[1] sys.path.insert(0, str(EXAMPLES)) from agents_examples import ag_ui_docs_copilot # noqa: E402 diff --git a/integrations/openai-agents/python/examples/uv.lock b/integrations/openai-agents/python/examples/uv.lock index 86f5766de0..3282c5a6e2 100644 --- a/integrations/openai-agents/python/examples/uv.lock +++ b/integrations/openai-agents/python/examples/uv.lock @@ -45,6 +45,11 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "ag-ui-openai-agents", editable = "../" }, @@ -53,6 +58,9 @@ requires-dist = [ { name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0" }] + [[package]] name = "ag-ui-protocol" version = "0.1.19" @@ -531,6 +539,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jiter" version = "0.16.0" @@ -720,6 +737,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/2e/2e96ca6928951fe1d16744c22dc1355eda1dd5b0dd920ca1d3ab602929f8/openai_agents-0.17.7-py3-none-any.whl", hash = "sha256:51b5ae43756eea37032e430f95979ba3999af6b1ade397df6c0ffeaf1939646a", size = 856074, upload-time = "2026-06-24T05:15:31.741Z" }, ] +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -874,6 +909,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyjwt" version = "2.13.0" @@ -891,6 +935,24 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1318,6 +1380,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "tqdm" version = "4.68.3" diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml index 957c4c9d19..3d0a164e04 100644 --- a/integrations/openai-agents/python/pyproject.toml +++ b/integrations/openai-agents/python/pyproject.toml @@ -20,6 +20,9 @@ dev = [ "pytest>=8.0", ] +[tool.pytest.ini_options] +testpaths = ["tests"] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index 9f3337ca18..f0fd9c3b33 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -6,7 +6,7 @@ ``HOSTED_TOOL_CALL_TYPES``). Members of the ``(str, Enum)`` classes compare equal to the raw wire strings (``StrEnum`` needs Python 3.11; this package supports 3.10+). The SDK's own ``Literal[...]`` annotations are the source of -truth for those — see ``tests/test_stream_types_drift.py``. Translator +truth for those — see ``tests/engine/test_types_drift.py``. Translator behavior lives in ``agui_to_openai.py`` and ``openai_to_agui.py``; this module only describes shapes and constants. """ diff --git a/integrations/openai-agents/python/tests/test_multimodal.py b/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py similarity index 100% rename from integrations/openai-agents/python/tests/test_multimodal.py rename to integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py diff --git a/integrations/openai-agents/python/tests/test_engine_mapping.py b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py similarity index 98% rename from integrations/openai-agents/python/tests/test_engine_mapping.py rename to integrations/openai-agents/python/tests/engine/test_openai_to_agui.py index dccd34cbb0..380bdc96f9 100644 --- a/integrations/openai-agents/python/tests/test_engine_mapping.py +++ b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py @@ -1,8 +1,8 @@ """ Tests for OpenAIToAGUITranslator's streaming event mapping. -The rest of the suite pins ids (test_snapshot) and the wire strings -(test_stream_types_drift); this one pins the actual translation: feed the +The rest of the suite pins IDs (test_openai_to_agui_snapshot) and wire strings +(test_types_drift); this one pins the actual translation: feed the engine the SDK stream events a real run emits — raw Responses deltas, run-item commits, agent-updated signals — and assert the exact AG-UI event sequence it yields. No network, no model; just the mapping. diff --git a/integrations/openai-agents/python/tests/test_snapshot.py b/integrations/openai-agents/python/tests/engine/test_openai_to_agui_snapshot.py similarity index 100% rename from integrations/openai-agents/python/tests/test_snapshot.py rename to integrations/openai-agents/python/tests/engine/test_openai_to_agui_snapshot.py diff --git a/integrations/openai-agents/python/tests/test_stream_types_drift.py b/integrations/openai-agents/python/tests/engine/test_types_drift.py similarity index 100% rename from integrations/openai-agents/python/tests/test_stream_types_drift.py rename to integrations/openai-agents/python/tests/engine/test_types_drift.py diff --git a/integrations/openai-agents/python/tests/test_translators.py b/integrations/openai-agents/python/tests/test_translator.py similarity index 99% rename from integrations/openai-agents/python/tests/test_translators.py rename to integrations/openai-agents/python/tests/test_translator.py index d3676d9901..d30b7c8d64 100644 --- a/integrations/openai-agents/python/tests/test_translators.py +++ b/integrations/openai-agents/python/tests/test_translator.py @@ -9,7 +9,8 @@ - ``to_agui`` always wraps the stream with RUN_STARTED / RUN_FINISHED / RUN_ERROR — not optional, thread_id/run_id come straight off run_input. - ``to_agui`` appends a MESSAGES_SNAPSHOT by default just before - RUN_FINISHED; snapshot content itself is covered in ``test_snapshot.py``, + RUN_FINISHED; snapshot content itself is covered in + ``engine/test_openai_to_agui_snapshot.py``, this file only checks the wiring (default on, ``emit_messages_snapshot=False`` opts out — same for bare iterators, since the snapshot no longer depends on ``result.new_items``). From f93c0ef9afb07eeb97eecf4a421138acde56666d Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Thu, 16 Jul 2026 01:48:14 +0200 Subject: [PATCH 63/94] fix(openai-agents): hosted call_id, content drops, audio gate, parent_run_id + example fixes --- apps/dojo/src/files.json | 2 +- integrations/openai-agents/python/README.md | 8 +-- .../human_in_the_loop_approval.py | 17 ++++-- .../python/examples/translator_server.py | 27 ++++++--- .../engine/agui_to_openai.py | 58 ++++++++++++++----- .../engine/openai_to_agui.py | 7 ++- .../src/ag_ui_openai_agents/translator.py | 2 + .../engine/test_agui_to_openai_multimodal.py | 53 ++++++++++++++++- .../tests/engine/test_openai_to_agui.py | 29 +++++++++- .../python/tests/test_translator.py | 13 +++++ 10 files changed, 178 insertions(+), 38 deletions(-) diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index b8be5f5bbb..a7a349b305 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4914,7 +4914,7 @@ }, { "name": "human_in_the_loop_approval.py", - "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n async def stream():\n decision = None\n if isinstance(body.forwarded_props, dict):\n decision = body.forwarded_props.get(\"approval\")\n\n pending_state = _pending_approvals.pop(body.thread_id, None)\n if decision and pending_state is not None:\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is not None:\n if decision.get(\"approve\"):\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_openai(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", + "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n async def stream():\n decision = None\n if isinstance(body.forwarded_props, dict):\n decision = body.forwarded_props.get(\"approval\")\n\n # Read without deleting: a request with a missing or mismatched\n # decision must not consume the paused run — a later correct\n # approval still has to be able to resume it.\n pending_state = _pending_approvals.get(body.thread_id)\n item = None\n if decision and pending_state is not None:\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is not None:\n if decision.get(\"approve\"):\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n del _pending_approvals[body.thread_id]\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_openai(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", "language": "python", "type": "file" } diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 5ad7161be8..e6127e1b8c 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -164,8 +164,8 @@ add_openai_agents_fastapi_endpoint(app, agent, "/") `Agent` is a config template, and client-declared tools are merged onto a per-request `clone()`, so concurrent requests never see each other's tools; - takes `run_config=...` to set run-wide model settings; -- exposes `run(RunAgentInput) -> AsyncIterator[BaseEvent]` if you want to serve - it on a transport other than FastAPI. +- exposes `run_streamed(RunAgentInput) -> AsyncIterator[BaseEvent]` if you want + to serve it on a transport other than FastAPI. ```python agent = OpenAIAgentsAgent( @@ -193,7 +193,7 @@ from ag_ui_openai_agents import ( |---|---|---| | `AGUITranslator` | translator (recommended) | compose it yourself — `to_openai` + `to_agui`; full control of the agent and server | | `TranslatedInput` | result type | what `translator.to_openai(...)` returns — `messages`/`tools` plus passthrough fields | -| `OpenAIAgentsAgent` | wrapper class | serve an agent: `run(RunAgentInput) -> AsyncIterator[BaseEvent]` | +| `OpenAIAgentsAgent` | wrapper class | serve an agent: `run_streamed(RunAgentInput) -> AsyncIterator[BaseEvent]` | | `add_openai_agents_fastapi_endpoint(app, agent, path)` | helper | wire a wrapped agent to FastAPI (SSE + `/health`) | The wrapper is built on the translator; it trades control for less code. The @@ -355,7 +355,7 @@ wrapped_agent = OpenAIAgentsAgent( | `emit_run_error` | `True` | Forwarded to `to_agui`. | | `run_error_message` | `None` | Forwarded to `to_agui`. | -Call `await` through its async iterator with `run(run_input)`. It yields +Call `await` through its async iterator with `run_streamed(run_input)`. It yields `BaseEvent` objects; you encode or transport them yourself unless you use the FastAPI helper. Client-owned tools are merged onto a clone for that run, so a tool declared by one request never changes the shared SDK agent or leaks into diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py index abcec9d4f1..c6ba0abb6a 100644 --- a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py @@ -92,7 +92,11 @@ async def stream(): if isinstance(body.forwarded_props, dict): decision = body.forwarded_props.get("approval") - pending_state = _pending_approvals.pop(body.thread_id, None) + # Read without deleting: a request with a missing or mismatched + # decision must not consume the paused run — a later correct + # approval still has to be able to resume it. + pending_state = _pending_approvals.get(body.thread_id) + item = None if decision and pending_state is not None: item = next( ( @@ -102,11 +106,12 @@ async def stream(): ), None, ) - if item is not None: - if decision.get("approve"): - pending_state.approve(item) - else: - pending_state.reject(item) + if item is not None: + if decision.get("approve"): + pending_state.approve(item) + else: + pending_state.reject(item) + del _pending_approvals[body.thread_id] result = Runner.run_streamed(agent, pending_state) else: translated = _translator.to_openai(body) diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index 9e4fd9dbbe..ee10ab62c2 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -138,7 +138,11 @@ async def _stream_approval(demo: DemoConfig, body: RunAgentInput): if isinstance(forwarded, dict): decision = forwarded.get("approval") - pending_state = _PENDING_APPROVALS.pop(body.thread_id, None) + # Read without deleting: a request with a missing or mismatched decision + # must not consume the paused run — a later correct approval still has to + # be able to resume it. + pending_state = _PENDING_APPROVALS.get(body.thread_id) + item = None if decision and pending_state is not None: item = next( ( @@ -148,17 +152,20 @@ async def _stream_approval(demo: DemoConfig, body: RunAgentInput): ), None, ) - if item is not None: - if decision.get("approve"): - pending_state.approve(item) - else: - pending_state.reject(item) + if item is not None: + if decision.get("approve"): + pending_state.approve(item) + else: + pending_state.reject(item) + del _PENDING_APPROVALS[body.thread_id] result = Runner.run_streamed(agent, pending_state) else: translated = translator.to_openai(body) if translated.tools: agent = agent.clone(tools=[*agent.tools, *translated.tools]) - result = Runner.run_streamed(agent, input=translated.messages) + result = Runner.run_streamed( + agent, input=translated.messages, context=translated.context + ) # result.interruptions is only known once the SDK's own stream is fully # drained — there's no mid-stream event for it. to_agui() always puts @@ -249,7 +256,11 @@ async def _stream(demo: DemoConfig, body: RunAgentInput): # snapshot, handles window bookkeeping + the final flush, and appends a # trailing MESSAGES_SNAPSHOT. Nothing to hand-emit here. try: - result = Runner.run_streamed(agent, input=translated.messages) + # context= carries the AG-UI context items through to run-context + # readers (dynamic_system_prompt resolves its language from them). + result = Runner.run_streamed( + agent, input=translated.messages, context=translated.context + ) async for ag_event in translator.to_agui(result, body, **to_agui_kwargs): yield encoder.encode(ag_event) except Exception: diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index 3f22d63c7b..28b2a9bb0f 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -173,7 +173,8 @@ def translate_message(self, message: Message) -> list[dict[str, Any]]: if isinstance(message, DeveloperMessage): return [self.translate_developer_message(message)] if isinstance(message, UserMessage): - return [self.translate_user_message(message)] + item = self.translate_user_message(message) + return [item] if item is not None else [] if isinstance(message, ReasoningMessage): item = self.translate_reasoning_message(message) return [item] if item is not None else [] @@ -217,21 +218,31 @@ def translate_developer_message(self, message: DeveloperMessage) -> dict[str, An "content": [{"type": "input_text", "text": message.content or ""}], } - def translate_user_message(self, message: UserMessage) -> dict[str, Any]: + def translate_user_message(self, message: UserMessage) -> dict[str, Any] | None: """Translate an AG-UI user turn into an SDK input item. - Content may be text or a list of typed multimodal parts. + Content may be text or a list of typed multimodal parts. A message + whose parts are all unsupported (e.g. video-only) is dropped whole — + sending a message with empty content would be rejected by the API. Args: message: The AG-UI user message. Returns: - {"type": "message", "role": "user", "content": [...]} + {"type": "message", "role": "user", "content": [...]}, or None + when nothing in the message could be translated. """ + content = self.translate_content(message.content) + if not content: + logger.debug( + "Dropping UserMessage id=%s: no translatable content parts", + message.id, + ) + return None return { "type": "message", "role": "user", - "content": self.translate_content(message.content), + "content": content, } def translate_reasoning_message( @@ -385,15 +396,18 @@ async def on_invoke_tool( def translate_content(self, content: Any) -> list[dict[str, Any]]: """Translate a message content field (str or list of parts) to input parts. - A string is wrapped as a single input_text part. A list has - each part passed through translate_content_part. Anything else - is best-effort stringified. + A string is wrapped as a single input_text part. A list has each part + passed through translate_content_part; parts that translate to None + are dropped, so an all-unsupported list (e.g. video-only) comes back + empty rather than stringified. Anything else is best-effort + stringified. Args: content: The message's content field. Returns: - List of Responses-API input parts. + List of Responses-API input parts; empty when every part was + unsupported. """ if isinstance(content, str): return [{"type": "input_text", "text": content}] @@ -403,8 +417,7 @@ def translate_content(self, content: Any) -> list[dict[str, Any]]: converted = self.translate_content_part(part) if converted is not None: parts.append(converted) - if parts: - return parts + return parts return [{"type": "input_text", "text": to_string(content)}] def translate_content_part(self, part: Any) -> dict[str, Any] | None: @@ -487,11 +500,15 @@ def translate_audio_content( if not value or source_type != "data": logger.debug("Dropping audio part: only data sources are supported") return None + audio_format = self._audio_format_from_mime(mime) + if audio_format is None: + logger.debug("Dropping audio part: unsupported format %s", mime) + return None return { "type": "input_audio", "input_audio": { "data": value, - "format": self._audio_format_from_mime(mime), + "format": audio_format, }, } @@ -615,14 +632,19 @@ def _data_source_to_url(source: Any) -> str | None: return None @staticmethod - def _audio_format_from_mime(mime: str | None) -> str: + def _audio_format_from_mime(mime: str | None) -> str | None: """Map a mime type to the format expected for Chat Completions audio. + Chat Completions only accepts "wav" and "mp3" for input audio, so + anything else maps to None and the caller drops the part — sending + e.g. "ogg" would get the whole request rejected. + Args: mime: The audio mime type, or None. Returns: - A short format tag (e.g. "wav", "mp3"). + "wav" or "mp3", or None for unsupported formats. A missing mime + defaults to "wav". """ if not mime: return "wav" @@ -632,7 +654,7 @@ def _audio_format_from_mime(mime: str | None) -> str: return "mp3" if subtype in ("x-wav", "wav", "wave"): return "wav" - return subtype + return None def _dispatch_dict_content_part( self, @@ -681,11 +703,15 @@ def _binary_as_audio( if not part.data: logger.debug("Dropping binary audio: URL-only audio not supported") return None + audio_format = self._audio_format_from_mime(mime) + if audio_format is None: + logger.debug("Dropping binary audio: unsupported format %s", mime) + return None return { "type": "input_audio", "input_audio": { "data": part.data, - "format": self._audio_format_from_mime(mime), + "format": audio_format, }, } diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index eaeb76fe8b..3b04ca15d2 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -320,7 +320,12 @@ def translate_output_item_added(self, data: Any) -> list[BaseEvent]: if item_type == OpenAIItemType.REASONING: return self._open_reasoning(key, self._resolve_id(item_id, new_reasoning_id)) if item_type in HOSTED_TOOL_CALL_TYPES: - call_id = self._resolve_id(item_id, new_tool_call_id) + # Some hosted calls (computer, shell, custom tools) carry a call_id + # distinct from the item id. The run-item commit reconciles by + # call_id, so open the window under it too — otherwise the same + # call streams twice, once per id, and the tool result attaches to + # the second copy instead of the one the client watched stream. + call_id = read_attr(item, "call_id") or self._resolve_id(item_id, new_tool_call_id) return self._open_tool_call(key, call_id, item_type) return [] diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index cfc0c94d22..dbc26ab871 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -129,6 +129,8 @@ async def to_agui( type=EventType.RUN_STARTED, thread_id=run_input.thread_id, run_id=run_input.run_id, + # Read defensively — older RunAgentInput versions lack the field. + parent_run_id=getattr(run_input, "parent_run_id", None), ) # 2. start_custom_event (optional) diff --git a/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py b/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py index 27cd1318c0..d9f3ccea40 100644 --- a/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py +++ b/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py @@ -19,6 +19,7 @@ InputContentDataSource, InputContentUrlSource, TextInputContent, + UserMessage, VideoInputContent, ) from ag_ui_openai_agents.engine.agui_to_openai import AGUIToOpenAITranslator @@ -90,8 +91,18 @@ def test_audio_format_from_mime_variants(): assert fmt("audio/x-wav") == "wav" assert fmt("audio/wav") == "wav" assert fmt("audio/wave") == "wav" - assert fmt("audio/ogg") == "ogg" assert fmt(None) == "wav" + # Chat Completions only takes wav/mp3 — anything else must map to None + # so the caller drops the part instead of sending a rejectable request. + assert fmt("audio/ogg") is None + assert fmt("audio/flac") is None + + +def test_audio_unsupported_format_is_dropped(): + part = AudioInputContent( + source=InputContentDataSource(value="Zm9v", mime_type="audio/ogg") + ) + assert _engine.translate_audio_content(part) is None # ── document ─────────────────────────────────────────────────────────── @@ -180,6 +191,11 @@ def test_binary_as_audio_without_data_is_dropped(): assert _engine._binary_as_audio(part, "audio/wav") is None +def test_binary_as_audio_unsupported_format_is_dropped(): + part = _binary(mime_type="audio/ogg", data="Zm9v") + assert _engine._binary_as_audio(part, "audio/ogg") is None + + def test_binary_as_file_url_source(): part = _binary(mime_type="application/pdf", url="https://x/y.pdf") out = _engine._binary_as_file(part, "application/pdf") @@ -238,6 +254,41 @@ def test_translate_content_part_dispatches_by_type(): } +# ── all-unsupported content: drop, never stringify ──────────────────────── + + +def test_content_list_of_only_unsupported_parts_translates_to_empty(): + # A video-only message must come back empty — not fall through to the + # stringify path and send the parts' repr to the model as input_text. + parts = [VideoInputContent(source=InputContentUrlSource(value="https://x/y.mp4"))] + assert _engine.translate_content(parts) == [] + + +def test_user_message_with_only_unsupported_parts_is_dropped_whole(): + message = UserMessage( + id="u1", + role="user", + content=[VideoInputContent(source=InputContentUrlSource(value="https://x/y.mp4"))], + ) + assert _engine.translate_user_message(message) is None + assert _engine.translate_message(message) == [] + + +def test_user_message_with_invalid_image_and_text_keeps_only_text(): + message = UserMessage( + id="u1", + role="user", + content=[ + TextInputContent(text="look"), + ImageInputContent( + source=InputContentDataSource(value="", mime_type="image/png") + ), + ], + ) + item = _engine.translate_user_message(message) + assert item["content"] == [{"type": "input_text", "text": "look"}] + + # ── end-to-end: mixed multimodal user message ───────────────────────────── diff --git a/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py index 380bdc96f9..a3f8dbd61d 100644 --- a/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py +++ b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py @@ -24,7 +24,7 @@ HandoffOutputItem, MCPApprovalRequestItem, ) -from agents.items import ToolCallOutputItem +from agents.items import ToolCallItem, ToolCallOutputItem from openai.types.responses import ResponseFunctionToolCall from openai.types.responses.response_output_item import McpApprovalRequest @@ -238,6 +238,33 @@ def test_function_call_args_delta_lazily_opens_the_call(): assert _types(events) == [EventType.TOOL_CALL_START, EventType.TOOL_CALL_ARGS] +def test_hosted_tool_call_with_distinct_call_id_streams_once(): + # Hosted calls like computer_call carry both an item id and a call_id. + # The raw window and the run-item commit must agree on the call_id — + # otherwise the same call streams two full lifecycles under two ids. + engine = OpenAIToAGUITranslator() + raw = SimpleNamespace(type="computer_call", id="comp_1", call_id="call_1") + events = _drive( + engine, + _added("computer_call", id="comp_1", call_id="call_1"), + _done("computer_call", id="comp_1", call_id="call_1"), + _run_item(ToolCallItem(agent=_AGENT, raw_item=raw)), + ) + assert _types(events) == [EventType.TOOL_CALL_START, EventType.TOOL_CALL_END] + assert {e.tool_call_id for e in events} == {"call_1"} + + +def test_hosted_tool_call_without_call_id_falls_back_to_item_id(): + engine = OpenAIToAGUITranslator() + events = _drive( + engine, + _added("web_search_call", id="ws_1"), + _done("web_search_call", id="ws_1"), + ) + assert _types(events) == [EventType.TOOL_CALL_START, EventType.TOOL_CALL_END] + assert {e.tool_call_id for e in events} == {"ws_1"} + + def test_tool_output_item_emits_tool_call_result(): engine = OpenAIToAGUITranslator() item = ToolCallOutputItem( diff --git a/integrations/openai-agents/python/tests/test_translator.py b/integrations/openai-agents/python/tests/test_translator.py index d30b7c8d64..00163fd4a0 100644 --- a/integrations/openai-agents/python/tests/test_translator.py +++ b/integrations/openai-agents/python/tests/test_translator.py @@ -151,6 +151,19 @@ async def collect(): ] +def test_streaming_to_agui_carries_parent_run_id_onto_run_started(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + run_input = _run_input() + nested_input = run_input.model_copy(update={"parent_run_id": "r0"}) + + async def collect(): + return [e async for e in translator.to_agui(_fake_stream(), nested_input)] + + events = asyncio.run(collect()) + assert isinstance(events[0], RunStartedEvent) + assert events[0].parent_run_id == "r0" + + def test_streaming_translator_is_reusable_fresh_engine_per_run(): translator = AGUITranslator(outbound_cls=_StubOutbound) before = _StubOutbound.instances From dfd647c32f05f10151d2ac866b801cabee9f499d Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Thu, 16 Jul 2026 01:59:04 +0200 Subject: [PATCH 64/94] fix(examples): preserve pending approval state --- apps/dojo/src/files.json | 2 +- .../human_in_the_loop_approval.py | 62 +++++--- .../examples/tests/test_translator_server.py | 135 ++++++++++++++++++ .../python/examples/translator_server.py | 36 ++--- 4 files changed, 192 insertions(+), 43 deletions(-) create mode 100644 integrations/openai-agents/python/examples/tests/test_translator_server.py diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index a7a349b305..c955c3ad95 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4914,7 +4914,7 @@ }, { "name": "human_in_the_loop_approval.py", - "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n async def stream():\n decision = None\n if isinstance(body.forwarded_props, dict):\n decision = body.forwarded_props.get(\"approval\")\n\n # Read without deleting: a request with a missing or mismatched\n # decision must not consume the paused run — a later correct\n # approval still has to be able to resume it.\n pending_state = _pending_approvals.get(body.thread_id)\n item = None\n if decision and pending_state is not None:\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is not None:\n if decision.get(\"approve\"):\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n del _pending_approvals[body.thread_id]\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_openai(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", + "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI, HTTPException\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\ndef _resolve_approval(\n pending_state: Any,\n forwarded_props: Any,\n) -> tuple[Any, bool | None]:\n \"\"\"Validate a decision for the paused run before an SSE response starts.\"\"\"\n decision = None\n if isinstance(forwarded_props, dict):\n decision = forwarded_props.get(\"approval\")\n\n if pending_state is None:\n if decision is not None:\n raise HTTPException(status_code=409, detail=\"No approval is pending\")\n return None, None\n\n if not isinstance(decision, dict):\n raise HTTPException(status_code=409, detail=\"This thread is waiting for approval\")\n\n call_id = decision.get(\"call_id\")\n approve = decision.get(\"approve\")\n if not call_id or not isinstance(approve, bool):\n raise HTTPException(status_code=409, detail=\"Invalid approval decision\")\n\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == call_id\n ),\n None,\n )\n if item is None:\n raise HTTPException(status_code=409, detail=\"Approval call_id does not match\")\n return item, approve\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n pending_state = _pending_approvals.get(body.thread_id)\n item, approve = _resolve_approval(pending_state, body.forwarded_props)\n\n async def stream():\n if item is not None:\n if approve:\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n del _pending_approvals[body.thread_id]\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_openai(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", "language": "python", "type": "file" } diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py index c6ba0abb6a..1f3038b8b2 100644 --- a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py @@ -35,8 +35,10 @@ from __future__ import annotations +from typing import Any + from agents import Agent, Runner, function_tool -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse from ag_ui.core import CustomEvent, EventType, RunAgentInput @@ -83,31 +85,51 @@ def create_human_in_the_loop_approval_agent() -> Agent: _pending_approvals: dict[str, object] = {} +def _resolve_approval( + pending_state: Any, + forwarded_props: Any, +) -> tuple[Any, bool | None]: + """Validate a decision for the paused run before an SSE response starts.""" + decision = None + if isinstance(forwarded_props, dict): + decision = forwarded_props.get("approval") + + if pending_state is None: + if decision is not None: + raise HTTPException(status_code=409, detail="No approval is pending") + return None, None + + if not isinstance(decision, dict): + raise HTTPException(status_code=409, detail="This thread is waiting for approval") + + call_id = decision.get("call_id") + approve = decision.get("approve") + if not call_id or not isinstance(approve, bool): + raise HTTPException(status_code=409, detail="Invalid approval decision") + + item = next( + ( + item + for item in pending_state.get_interruptions() + if getattr(item.raw_item, "call_id", None) == call_id + ), + None, + ) + if item is None: + raise HTTPException(status_code=409, detail="Approval call_id does not match") + return item, approve + + @app.post("/") async def run(body: RunAgentInput) -> StreamingResponse: """Run or resume the approval-gated agent.""" + pending_state = _pending_approvals.get(body.thread_id) + item, approve = _resolve_approval(pending_state, body.forwarded_props) + async def stream(): - decision = None - if isinstance(body.forwarded_props, dict): - decision = body.forwarded_props.get("approval") - - # Read without deleting: a request with a missing or mismatched - # decision must not consume the paused run — a later correct - # approval still has to be able to resume it. - pending_state = _pending_approvals.get(body.thread_id) - item = None - if decision and pending_state is not None: - item = next( - ( - item - for item in pending_state.get_interruptions() - if getattr(item.raw_item, "call_id", None) == decision.get("call_id") - ), - None, - ) if item is not None: - if decision.get("approve"): + if approve: pending_state.approve(item) else: pending_state.reject(item) diff --git a/integrations/openai-agents/python/examples/tests/test_translator_server.py b/integrations/openai-agents/python/examples/tests/test_translator_server.py new file mode 100644 index 0000000000..7c2e850e65 --- /dev/null +++ b/integrations/openai-agents/python/examples/tests/test_translator_server.py @@ -0,0 +1,135 @@ +"""Regression tests for the direct-translator example server.""" + +from __future__ import annotations + +import asyncio +import sys +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest +from fastapi import HTTPException + +EXAMPLES = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EXAMPLES)) + +import translator_server # noqa: E402 +from agents_examples import human_in_the_loop_approval # noqa: E402 +from ag_ui.core import Context, RunAgentInput, UserMessage # noqa: E402 + + +class _PendingState: + def __init__(self, call_id: str = "call_1") -> None: + self.item = SimpleNamespace(raw_item=SimpleNamespace(call_id=call_id)) + + def get_interruptions(self) -> list[Any]: + return [self.item] + + +def _run_input(forwarded_props: Any = None) -> RunAgentInput: + return RunAgentInput( + thread_id="thread_1", + run_id="run_1", + messages=[UserMessage(id="message_1", role="user", content="hello")], + tools=[], + state={}, + context=[], + forwarded_props=forwarded_props, + ) + + +@pytest.mark.parametrize( + "forwarded_props", + [ + None, + {"approval": {"call_id": "wrong_call", "approve": True}}, + {"approval": {"call_id": "call_1"}}, + ], +) +@pytest.mark.parametrize( + ("endpoint", "store"), + [ + (human_in_the_loop_approval.run, human_in_the_loop_approval._pending_approvals), + ( + lambda body: translator_server.run("human_in_the_loop_approval", body), + translator_server._PENDING_APPROVALS, + ), + ], +) +def test_invalid_approval_keeps_the_pending_run( + forwarded_props: Any, + endpoint: Any, + store: dict[str, object], +) -> None: + state = _PendingState() + store.clear() + store["thread_1"] = state + + try: + with pytest.raises(HTTPException) as exc_info: + asyncio.run(endpoint(_run_input(forwarded_props))) + + assert exc_info.value.status_code == 409 + assert store["thread_1"] is state + finally: + store.clear() + + +def test_valid_approval_resolves_the_matching_item() -> None: + state = _PendingState() + + item, approve = human_in_the_loop_approval._resolve_approval( + state, + {"approval": {"call_id": "call_1", "approve": True}}, + ) + + assert item is state.item + assert approve is True + + +def test_approval_without_a_pending_run_is_rejected() -> None: + with pytest.raises(HTTPException) as exc_info: + human_in_the_loop_approval._resolve_approval( + None, + {"approval": {"call_id": "call_1", "approve": True}}, + ) + + assert exc_info.value.status_code == 409 + + +def test_direct_server_forwards_context_to_the_sdk(monkeypatch: pytest.MonkeyPatch) -> None: + context = [Context(description="Response language", value="German")] + body = _run_input() + body.context = context + translated = SimpleNamespace(messages=[{"role": "user"}], tools=[], context=context) + captured: dict[str, Any] = {} + result = object() + + async def to_agui(*args: Any, **kwargs: Any): + if False: + yield None + + fake_translator = SimpleNamespace( + to_openai=lambda run_input: translated, + to_agui=to_agui, + ) + + def run_streamed(agent: Any, *, input: Any, context: Any) -> object: + captured.update(agent=agent, input=input, context=context) + return result + + monkeypatch.setattr(translator_server, "translator", fake_translator) + monkeypatch.setattr(translator_server.Runner, "run_streamed", run_streamed) + + demo = SimpleNamespace( + agent=object(), + start_custom_event=None, + end_custom_event=None, + ) + + async def collect() -> list[Any]: + return [chunk async for chunk in translator_server._stream(demo, body)] + + assert asyncio.run(collect()) == [] + assert captured["context"] is context diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index ee10ab62c2..9fd6bb1daf 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -60,6 +60,7 @@ import logging import os from pathlib import Path +from typing import Any import uvicorn from agents import Runner @@ -70,6 +71,7 @@ from ag_ui.encoder import EventEncoder from ag_ui_openai_agents import AGUITranslator from agents_examples import DemoConfig, build_registry +from agents_examples.human_in_the_loop_approval import _resolve_approval logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -118,13 +120,22 @@ async def run(agent_name: str, body: RunAgentInput) -> StreamingResponse: body.run_id, ) if agent_name == "human_in_the_loop_approval": + pending_state = _PENDING_APPROVALS.get(body.thread_id) + item, approve = _resolve_approval(pending_state, body.forwarded_props) return StreamingResponse( - _stream_approval(demo, body), media_type=encoder.get_content_type() + _stream_approval(demo, body, pending_state, item, approve), + media_type=encoder.get_content_type(), ) return StreamingResponse(_stream(demo, body), media_type=encoder.get_content_type()) -async def _stream_approval(demo: DemoConfig, body: RunAgentInput): +async def _stream_approval( + demo: DemoConfig, + body: RunAgentInput, + pending_state: Any, + item: Any, + approve: bool | None, +): """Same shape as _stream, plus resuming from result.interruptions. Kept separate from _stream rather than adding an if-branch there: this is @@ -133,27 +144,8 @@ async def _stream_approval(demo: DemoConfig, body: RunAgentInput): translated.messages, and that logic doesn't apply to any other demo. """ agent = demo.agent - decision = None - forwarded = body.forwarded_props - if isinstance(forwarded, dict): - decision = forwarded.get("approval") - - # Read without deleting: a request with a missing or mismatched decision - # must not consume the paused run — a later correct approval still has to - # be able to resume it. - pending_state = _PENDING_APPROVALS.get(body.thread_id) - item = None - if decision and pending_state is not None: - item = next( - ( - i - for i in pending_state.get_interruptions() - if getattr(i.raw_item, "call_id", None) == decision.get("call_id") - ), - None, - ) if item is not None: - if decision.get("approve"): + if approve: pending_state.approve(item) else: pending_state.reject(item) From 35516247af97e65b625bc5d9af087a637a8b7db8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Fri, 17 Jul 2026 00:41:53 +0200 Subject: [PATCH 65/94] fix(openai-agents): search docs by section and harden approval flow --- .../openai-agents/python/examples/README.md | 27 +++- .../agents_examples/ag_ui_docs_copilot.py | 119 +++++++++------ .../examples/agents_examples/docs_search.py | 143 ++++++++++++++++++ .../human_in_the_loop_approval.py | 98 ++++++++---- .../examples/tests/test_ag_ui_docs_copilot.py | 36 +++++ .../tests/test_human_in_the_loop_approval.py | 91 +++++++++++ .../examples/tests/test_translator_server.py | 92 ++++++++--- .../python/examples/translator_server.py | 32 ++-- 8 files changed, 525 insertions(+), 113 deletions(-) create mode 100644 integrations/openai-agents/python/examples/agents_examples/docs_search.py create mode 100644 integrations/openai-agents/python/examples/tests/test_human_in_the_loop_approval.py diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 4420adb970..5401ee20d6 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -71,11 +71,12 @@ lists every registered agent. Demos map 1:1 onto the AG-UI Dojo feature pages. The main Copilot handles normal conversation without carrying the documentation in its instructions. Documentation and code questions are delegated to an -`AG-UI OpenAI Agents Specialist` and `AG-UI Protocol Python Specialist`, which -receive the integration and core SDK README files through the SDK's -`Agent.as_tool()` API. Its endpoint keeps the direct `to_openai` → -`Runner.run_streamed` → `to_agui` flow visible and adds no retrieval framework, -vector database, or network dependency. +`AG-UI OpenAI Agents Specialist` and `AG-UI Protocol Python Specialist` through +the SDK's `Agent.as_tool()` API. Each specialist searches a local Markdown +index first, so only the highest-ranked README sections enter the model context +instead of the complete documentation. The retrieval is deterministic and adds +no vector database, embedding request, or network dependency. The endpoint +keeps the direct `to_openai` → `Runner.run_streamed` → `to_agui` flow visible. **Try:** `"Explain how to connect my existing OpenAI Agents SDK agent to AG-UI, then ask the Documentation Specialist for the smallest FastAPI streaming @@ -144,10 +145,24 @@ through the shared loop: drops anything that arrives once a run is marked finished). 2. The client's decision comes back on the _next_ request as `RunAgentInput.forwarded_props["approval"]` (`{"call_id", "approve"}`). - The server looks up the stored state, calls `state.approve()` / + `resolve_approval()` claims the stored state, calls `state.approve()` / `state.reject()`, and resumes with `Runner.run_streamed(agent, state)` instead of starting over from `translated.messages`. +`resolve_approval()` takes the paused run out of the store before deciding +anything, so a double-clicked Approve can't resume the same run twice — the +second request finds nothing to claim. A request that _isn't_ a matching +decision (a plain message, a stale `call_id`, an `approve` that isn't a real +bool — `"false"` is a truthy string) abandons the paused run and answers +normally rather than refusing: the user moved on, and a thread whose approval +card got lost on a page refresh should still be usable. + +Both servers drain the SDK stream by hand here, which puts that part outside +`to_agui`'s error handling. A failure mid-drain is kept and re-raised from the +replay instead of reported on the spot, so it lands back inside `to_agui` and +the route behaves like every other one: whatever already streamed still +reaches the client, open sequences close, and `RUN_ERROR` goes out last. + **Try:** `"I'd like a refund for ORD-1001."` — requires a client that renders `approval_request` custom events and sends the decision back via `forwarded_props` (the AG-UI Dojo's Human in the Loop (Backend Approval) diff --git a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py index 26bbf9c48e..7a700e59e4 100644 --- a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py +++ b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py @@ -2,9 +2,9 @@ from __future__ import annotations -from pathlib import Path +from importlib.metadata import metadata -from agents import Agent, Runner +from agents import Agent, Runner, function_tool from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse @@ -12,16 +12,39 @@ from ag_ui.encoder import EventEncoder from ag_ui_openai_agents import AGUITranslator from .constants import DEFAULT_MODEL +from .docs_search import MarkdownSearchIndex -# ag-ui-protocol specialist: knows the core Python SDK documentation -AG_UI_PROTOCOL_DOCS = Path(__file__).resolve().parents[5].joinpath( - "sdks/python/README.md" -).read_text(encoding="utf-8") -AG_UI_PROTOCOL_DOCS_INSTRUCTIONS = f"""You are the technical specialist for -AG-UI Protocol's core Python SDK. The documentation below is your source of -truth. Help developers understand the ag_ui.core data models, RunAgentInput, -messages, events, event types, and ag_ui.encoder EventEncoder, including how to -create and stream protocol events. + +def _load_distribution_readme(distribution: str) -> str: + """Load the installed package README from its distribution metadata.""" + document = metadata(distribution).get_payload() + if not isinstance(document, str) or not document.strip(): + raise RuntimeError(f"{distribution} has no README in its package metadata") + return document + + +# ag-ui-protocol specialist: knows the core Python SDK documentation. +AG_UI_PROTOCOL_DOCS = _load_distribution_readme("ag-ui-protocol") +_AG_UI_PROTOCOL_DOCS_INDEX = MarkdownSearchIndex(AG_UI_PROTOCOL_DOCS) + + +@function_tool +def search_ag_ui_protocol_docs(query: str) -> str: + """Find relevant AG-UI Protocol Python documentation sections. + + Args: + query: The documentation question or API concepts to find. + """ + return _AG_UI_PROTOCOL_DOCS_INDEX.search(query) + + +AG_UI_PROTOCOL_DOCS_INSTRUCTIONS = """You are the technical specialist for +AG-UI Protocol's core Python SDK. Help developers understand ag_ui.core data +models, RunAgentInput, messages, events, event types, and EventEncoder. + +Before answering, always call search_ag_ui_protocol_docs with a focused query. +Treat its excerpts as your only source of truth. If the first search is +insufficient, search once more with different API terms. Answer only the user's question. Retrieve and explain only the relevant parts of the documentation; do not add a broad tutorial or unrelated integration @@ -30,30 +53,41 @@ followed by a short explanation. Use documented APIs only. If the documentation does not establish an answer, say so briefly instead of guessing. - - -{AG_UI_PROTOCOL_DOCS} - """ ag_ui_protocol_docs_agent = Agent( name="AG-UI Protocol Python Specialist", model=DEFAULT_MODEL, instructions=AG_UI_PROTOCOL_DOCS_INSTRUCTIONS, + tools=[search_ag_ui_protocol_docs], ) -# ag-ui-openai-agents specialist: knows the integration documentation -AG_UI_OPENAI_AGENTS_DOCS = Path(__file__).resolve().parents[2].joinpath( - "README.md" -).read_text(encoding="utf-8") -AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS = f"""You are the technical specialist for -AG-UI OpenAI Agents integration. The documentation below is your source of -truth. Help developers understand and implement the integration: the +# ag-ui-openai-agents specialist: knows the integration documentation. +AG_UI_OPENAI_AGENTS_DOCS = _load_distribution_readme("ag-ui-openai-agents") +_AG_UI_OPENAI_AGENTS_DOCS_INDEX = MarkdownSearchIndex(AG_UI_OPENAI_AGENTS_DOCS) + + +@function_tool +def search_ag_ui_openai_agents_docs(query: str) -> str: + """Find relevant AG-UI OpenAI Agents integration documentation sections. + + Args: + query: The documentation question or integration concepts to find. + """ + return _AG_UI_OPENAI_AGENTS_DOCS_INDEX.search(query) + + +AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS = """You are the technical specialist for +the AG-UI OpenAI Agents integration. Help developers understand the AGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE endpoints, tools, client tools, context, state, lifecycle events, errors, and testing. +Before answering, always call search_ag_ui_openai_agents_docs with a focused +query. Treat its excerpts as your only source of truth. If the first search is +insufficient, search once more with different API terms. + Answer only the user's question. Retrieve and explain only the relevant parts of the documentation; do not add a broad tutorial, related features, or extra options unless the user asks. Be concise and practical. For code requests, @@ -61,20 +95,16 @@ for the request, followed by a short explanation. Use documented APIs only. Do not invent behavior or configuration. If the documentation does not establish an answer, say so briefly instead of guessing. - - -{AG_UI_OPENAI_AGENTS_DOCS} - """ ag_ui_openai_agents_docs_agent = Agent( name="AG-UI OpenAI Agents Specialist", model=DEFAULT_MODEL, instructions=AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS, + tools=[search_ag_ui_openai_agents_docs], ) - # Main Copilot: handles normal conversation and delegates documentation work. copilot_instructions = """You are the developer-facing Copilot for an AG-UI application. Answer only what the user asks. Be clear, practical, and concise; @@ -92,31 +122,32 @@ requests, make sure the relevant specialist is called.""" copilot_agent = Agent( - name="AG-UI Docs Copilot", - model=DEFAULT_MODEL, - instructions=copilot_instructions, - tools=[ - ag_ui_protocol_docs_agent.as_tool( - tool_name="ask_ag_ui_protocol_docs", - tool_description=( - "Provide authoritative AG-UI core Python SDK guidance about " - "protocol types, RunAgentInput, events, and EventEncoder." - ), + name="AG-UI Docs Copilot", + model=DEFAULT_MODEL, + instructions=copilot_instructions, + tools=[ + ag_ui_protocol_docs_agent.as_tool( + tool_name="ask_ag_ui_protocol_docs", + tool_description=( + "Provide authoritative AG-UI core Python SDK guidance about " + "protocol types, RunAgentInput, events, and EventEncoder." ), - ag_ui_openai_agents_docs_agent.as_tool( - tool_name="ask_ag_ui_openai_agents_docs", - tool_description=( - "Provide authoritative AG-UI and OpenAI Agents SDK guidance, " - "including documented Python integration snippets." - ), + ), + ag_ui_openai_agents_docs_agent.as_tool( + tool_name="ask_ag_ui_openai_agents_docs", + tool_description=( + "Provide authoritative AG-UI and OpenAI Agents SDK guidance, " + "including documented Python integration snippets." ), - ], + ), + ], ) # AGUI Translator Integration app = FastAPI(title="AG-UI Docs Copilot") translator = AGUITranslator() + @app.post("/") async def run_ag_ui_docs_copilot( body: RunAgentInput, request: Request diff --git a/integrations/openai-agents/python/examples/agents_examples/docs_search.py b/integrations/openai-agents/python/examples/agents_examples/docs_search.py new file mode 100644 index 0000000000..4f06dd5b38 --- /dev/null +++ b/integrations/openai-agents/python/examples/agents_examples/docs_search.py @@ -0,0 +1,143 @@ +"""Small, dependency-free Markdown retrieval for the docs Copilot example.""" + +from __future__ import annotations + +import re +from collections import Counter +from dataclasses import dataclass +from math import log + + +_WORD_RE = re.compile(r"[a-z0-9]+") +_HEADING_RE = re.compile(r"^(#{1,6})\s+(.+)$") +_STOP_WORDS = { + "a", + "an", + "about", + "and", + "are", + "do", + "for", + "from", + "how", + "i", + "in", + "is", + "it", + "of", + "on", + "the", + "to", + "use", + "what", + "with", +} + + +def _terms(value: str) -> list[str]: + """Normalize prose and Python identifiers into searchable terms.""" + value = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", value).replace("_", "-") + return [term for term in _WORD_RE.findall(value.lower()) if term not in _STOP_WORDS] + + +@dataclass(frozen=True) +class _Section: + heading: str + content: str + heading_terms: frozenset[str] + content_terms: tuple[str, ...] + + +class MarkdownSearchIndex: + """Rank Markdown sections with BM25 and return the relevant excerpts.""" + + def __init__(self, document: str) -> None: + self._sections = self._split_sections(document) + self._document_frequency = Counter( + term for section in self._sections for term in set(section.content_terms) + ) + self._average_section_length = ( + sum(len(section.content_terms) for section in self._sections) + / len(self._sections) + if self._sections + else 0.0 + ) + + @staticmethod + def _split_sections(document: str) -> list[_Section]: + sections: list[_Section] = [] + heading = "Overview" + lines: list[str] = [] + + def append_section() -> None: + content = "\n".join(lines).strip() + if not content: + return + sections.append( + _Section( + heading=heading, + content=content, + heading_terms=frozenset(_terms(heading)), + content_terms=tuple(_terms(content)), + ) + ) + + for line in document.splitlines(): + match = _HEADING_RE.match(line) + if match: + append_section() + heading = match.group(2).strip() + lines = [line] + else: + lines.append(line) + append_section() + return sections + + def search(self, query: str, *, limit: int = 3, max_chars: int = 7_000) -> str: + """Return the highest-scoring sections within a character budget.""" + query_terms = set(_terms(query)) + if not query_terms: + return "No specific documentation terms were supplied." + + ranked: list[tuple[float, int, _Section]] = [] + for position, section in enumerate(self._sections): + frequencies = Counter(section.content_terms) + score = 0.0 + for term in query_terms: + frequency = frequencies[term] + if not frequency: + continue + document_frequency = self._document_frequency[term] + inverse_frequency = log( + 1 + + (len(self._sections) - document_frequency + 0.5) + / (document_frequency + 0.5) + ) + length_ratio = len(section.content_terms) / self._average_section_length + saturation = (frequency * 2.2) / ( + frequency + 1.2 * (0.25 + 0.75 * length_ratio) + ) + score += inverse_frequency * saturation + if term in section.heading_terms: + score += inverse_frequency * 2 + if score: + ranked.append((score, -position, section)) + + if not ranked: + return "No relevant section was found in this documentation source." + + excerpts: list[str] = [] + used_chars = 0 + for _, _, section in sorted(ranked, reverse=True)[:limit]: + separator_size = 2 if excerpts else 0 + if used_chars + separator_size + len(section.content) > max_chars: + continue + excerpts.append(section.content) + used_chars += separator_size + len(section.content) + + if not excerpts: + return "The closest documentation section exceeds the retrieval budget." + return "\n\n".join(excerpts) + + +__all__ = ["MarkdownSearchIndex"] diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py index 1f3038b8b2..da52703553 100644 --- a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py @@ -35,17 +35,21 @@ from __future__ import annotations +import logging from typing import Any from agents import Agent, Runner, function_tool -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI from fastapi.responses import StreamingResponse from ag_ui.core import CustomEvent, EventType, RunAgentInput from ag_ui.encoder import EventEncoder from ag_ui_openai_agents import AGUITranslator +from ag_ui_openai_agents.engine import ClientToolPending from .constants import DEFAULT_MODEL +logger = logging.getLogger(__name__) + # Fake order book — good enough to make "approved" visibly do something. _ORDERS: dict[str, dict] = { "ORD-1001": {"amount": 49.99, "status": "paid"}, @@ -85,47 +89,65 @@ def create_human_in_the_loop_approval_agent() -> Agent: _pending_approvals: dict[str, object] = {} -def _resolve_approval( - pending_state: Any, +def resolve_approval( + store: dict[str, Any], + thread_id: str, forwarded_props: Any, -) -> tuple[Any, bool | None]: - """Validate a decision for the paused run before an SSE response starts.""" +) -> tuple[Any, Any, bool]: + """Claim the paused run for this request when a matching decision arrived. + + Pops first: whoever gets here wins, so a double-clicked Approve can't + resume the same run twice. Anything other than a decision that matches a + pending interruption abandons the paused run and starts a fresh turn — + the user moved on, and a thread should never be stuck waiting forever. + + ``approve`` has to be a real bool. Truthiness would read the string + "false" as approval and run a refund the user declined, so a malformed + decision is treated as no decision at all. + + Args: + store: thread_id -> paused RunState. + thread_id: The thread this request belongs to. + forwarded_props: The request's forwarded_props. + + Returns: + (pending_state, item, approve) to resume, or (None, None, False) to + run the request fresh. + """ + pending_state = store.pop(thread_id, None) + if pending_state is None: + return None, None, False + decision = None if isinstance(forwarded_props, dict): decision = forwarded_props.get("approval") - - if pending_state is None: - if decision is not None: - raise HTTPException(status_code=409, detail="No approval is pending") - return None, None - if not isinstance(decision, dict): - raise HTTPException(status_code=409, detail="This thread is waiting for approval") + return None, None, False - call_id = decision.get("call_id") approve = decision.get("approve") - if not call_id or not isinstance(approve, bool): - raise HTTPException(status_code=409, detail="Invalid approval decision") + if not isinstance(approve, bool): + return None, None, False item = next( ( item for item in pending_state.get_interruptions() - if getattr(item.raw_item, "call_id", None) == call_id + if getattr(item.raw_item, "call_id", None) == decision.get("call_id") ), None, ) if item is None: - raise HTTPException(status_code=409, detail="Approval call_id does not match") - return item, approve + return None, None, False + return pending_state, item, approve @app.post("/") async def run(body: RunAgentInput) -> StreamingResponse: """Run or resume the approval-gated agent.""" - pending_state = _pending_approvals.get(body.thread_id) - item, approve = _resolve_approval(pending_state, body.forwarded_props) + pending_state, item, approve = resolve_approval( + _pending_approvals, body.thread_id, body.forwarded_props + ) async def stream(): if item is not None: @@ -133,7 +155,6 @@ async def stream(): pending_state.approve(item) else: pending_state.reject(item) - del _pending_approvals[body.thread_id] result = Runner.run_streamed(agent, pending_state) else: translated = _translator.to_openai(body) @@ -144,9 +165,23 @@ async def stream(): run_agent, input=translated.messages, context=translated.context ) - raw_events = [event async for event in result.stream_events()] + # Collect as we go rather than in one comprehension: whatever streamed + # before a mid-drain stop still has to reach the client. A client-owned + # tool ends the run cleanly here; a real failure is kept and re-raised + # from replay() below, so to_agui sees it and handles it the same way + # it would on any other route. + raw_events = [] + stream_error = None + try: + async for event in result.stream_events(): + raw_events.append(event) + except ClientToolPending: + pass + except Exception as exc: + stream_error = exc + end_custom_event = None - if result.interruptions: + if stream_error is None and result.interruptions: _pending_approvals[body.thread_id] = result.to_state() end_custom_event = CustomEvent( type=EventType.CUSTOM, @@ -164,10 +199,17 @@ async def stream(): async def replay(): for event in raw_events: yield event - - async for event in _translator.to_agui( - replay(), body, end_custom_event=end_custom_event - ): - yield _encoder.encode(event) + if stream_error is not None: + raise stream_error + + try: + async for event in _translator.to_agui( + replay(), body, end_custom_event=end_custom_event + ): + yield _encoder.encode(event) + except Exception: + # to_agui already sent RUN_ERROR before re-raising; log the real + # traceback here rather than let it escape the response. + logger.exception("Agent run failed") return StreamingResponse(stream(), media_type=_encoder.get_content_type()) diff --git a/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py b/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py index 69684d38fc..64969dcbd1 100644 --- a/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py +++ b/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py @@ -10,6 +10,7 @@ sys.path.insert(0, str(EXAMPLES)) from agents_examples import ag_ui_docs_copilot # noqa: E402 +from agents_examples.docs_search import MarkdownSearchIndex # noqa: E402 def test_docs_copilot_loads_the_integration_readme() -> None: @@ -33,6 +34,41 @@ def test_docs_copilot_has_a_documentation_specialist_tool() -> None: ag_ui_docs_copilot.ag_ui_protocol_docs_agent.name == "AG-UI Protocol Python Specialist" ) + assert { + tool.name for tool in ag_ui_docs_copilot.ag_ui_openai_agents_docs_agent.tools + } == {"search_ag_ui_openai_agents_docs"} + assert {tool.name for tool in ag_ui_docs_copilot.ag_ui_protocol_docs_agent.tools} == { + "search_ag_ui_protocol_docs" + } + + +def test_docs_copilot_retrieves_only_relevant_readme_sections() -> None: + excerpts = ag_ui_docs_copilot._AG_UI_OPENAI_AGENTS_DOCS_INDEX.search( + "How do I stream AGUITranslator events from a FastAPI endpoint?" + ) + + assert "Runner.run_streamed" in excerpts + assert len(excerpts) < len(ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS) + assert ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS not in ( + ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS + ) + assert ag_ui_docs_copilot.AG_UI_PROTOCOL_DOCS not in ( + ag_ui_docs_copilot.AG_UI_PROTOCOL_DOCS_INSTRUCTIONS + ) + + approval_excerpts = ag_ui_docs_copilot._AG_UI_OPENAI_AGENTS_DOCS_INDEX.search( + "How do I resume an interrupted run after approval?" + ) + assert "## Backend tool approval (`needs_approval`)" in approval_excerpts + + +def test_markdown_search_handles_empty_documents() -> None: + for document in ("", " \n\t"): + index = MarkdownSearchIndex(document) + assert ( + index.search("AGUITranslator") + == "No relevant section was found in this documentation source." + ) def test_docs_copilot_keeps_the_direct_translator_flow_visible() -> None: diff --git a/integrations/openai-agents/python/examples/tests/test_human_in_the_loop_approval.py b/integrations/openai-agents/python/examples/tests/test_human_in_the_loop_approval.py new file mode 100644 index 0000000000..a741bb28a9 --- /dev/null +++ b/integrations/openai-agents/python/examples/tests/test_human_in_the_loop_approval.py @@ -0,0 +1,91 @@ +"""Tests for the approval demo's decision handling. + +resolve_approval is the gate in front of a paused run: it decides whether a +request resumes that run or starts a fresh turn. No model, no network — just +the decision logic, driven with a stub RunState. +""" + +from __future__ import annotations + +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +EXAMPLES = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EXAMPLES)) + +from agents_examples.human_in_the_loop_approval import resolve_approval # noqa: E402 + + +class _StubState: + """Stands in for a paused RunState carrying one interruption.""" + + def __init__(self, call_id: str = "call_1") -> None: + self.item = SimpleNamespace(raw_item=SimpleNamespace(call_id=call_id)) + + def get_interruptions(self) -> list: + return [self.item] + + +def _decision(call_id: str = "call_1", approve: bool = True) -> dict: + return {"approval": {"call_id": call_id, "approve": approve}} + + +def test_matching_decision_resumes_and_claims_the_paused_run(): + state = _StubState() + store = {"t1": state} + + pending_state, item, approve = resolve_approval(store, "t1", _decision()) + + assert pending_state is state + assert item is state.item + assert approve is True + assert store == {}, "the claimed run must not stay behind for a second request" + + +def test_reject_decision_resumes_with_approve_false(): + state = _StubState() + _, item, approve = resolve_approval({"t1": state}, "t1", _decision(approve=False)) + assert item is state.item + assert approve is False + + +def test_second_concurrent_decision_finds_nothing_to_resume(): + # Both requests validate before either runs, so the store is the only thing + # keeping a double-clicked Approve from resuming the same run twice. + store = {"t1": _StubState()} + first = resolve_approval(store, "t1", _decision()) + second = resolve_approval(store, "t1", _decision()) + + assert first[1] is not None + assert second == (None, None, False) + + +def test_plain_message_starts_a_fresh_turn_instead_of_wedging_the_thread(): + # The user typed something else instead of deciding. That abandons the + # paused run rather than blocking every later message on the thread. + store = {"t1": _StubState()} + + assert resolve_approval(store, "t1", None) == (None, None, False) + assert store == {} + + +@pytest.mark.parametrize("approve", ["false", "true", 0, 1, None, "", "yes"]) +def test_a_non_boolean_approve_never_resumes_the_run(approve): + # "false" is a truthy string — reading it as approval would refund an order + # the user declined. Only a real bool decides anything. + store = {"t1": _StubState()} + assert resolve_approval( + store, "t1", {"approval": {"call_id": "call_1", "approve": approve}} + ) == (None, None, False) + + +def test_decision_for_an_unknown_call_id_starts_fresh(): + store = {"t1": _StubState(call_id="call_1")} + assert resolve_approval(store, "t1", _decision(call_id="other")) == (None, None, False) + + +def test_decision_with_no_pending_run_starts_fresh(): + assert resolve_approval({}, "t1", _decision()) == (None, None, False) diff --git a/integrations/openai-agents/python/examples/tests/test_translator_server.py b/integrations/openai-agents/python/examples/tests/test_translator_server.py index 7c2e850e65..bc0c2255e7 100644 --- a/integrations/openai-agents/python/examples/tests/test_translator_server.py +++ b/integrations/openai-agents/python/examples/tests/test_translator_server.py @@ -9,7 +9,6 @@ from typing import Any import pytest -from fastapi import HTTPException EXAMPLES = Path(__file__).resolve().parents[1] sys.path.insert(0, str(EXAMPLES)) @@ -57,45 +56,88 @@ def _run_input(forwarded_props: Any = None) -> RunAgentInput: ), ], ) -def test_invalid_approval_keeps_the_pending_run( +def test_a_request_without_a_valid_decision_starts_a_fresh_run( forwarded_props: Any, endpoint: Any, store: dict[str, object], ) -> None: - state = _PendingState() + # Both servers answer normally instead of refusing the request: a user who + # types something else instead of deciding abandons the paused run, and a + # thread that lost its approval card can still be talked to. store.clear() - store["thread_1"] = state + store["thread_1"] = _PendingState() try: - with pytest.raises(HTTPException) as exc_info: - asyncio.run(endpoint(_run_input(forwarded_props))) + response = asyncio.run(endpoint(_run_input(forwarded_props))) - assert exc_info.value.status_code == 409 - assert store["thread_1"] is state + assert response.status_code == 200 + assert store == {} finally: store.clear() -def test_valid_approval_resolves_the_matching_item() -> None: - state = _PendingState() - - item, approve = human_in_the_loop_approval._resolve_approval( - state, - {"approval": {"call_id": "call_1", "approve": True}}, +def test_approval_stream_error_keeps_what_already_streamed( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # The hand-drain is the only place an SDK failure can land outside + # to_agui's wrapper. Re-raising it from the replay puts it back inside, so + # the client still gets the events that made it out before the failure — + # and one RUN_ERROR, built by the translator like on every other route. + boom = RuntimeError("provider exploded") + + async def stream_events(): + yield "event_1" + raise boom + + result = SimpleNamespace( + stream_events=stream_events, + interruptions=[], + to_state=lambda: None, + ) + monkeypatch.setattr( + translator_server.Runner, "run_streamed", lambda *a, **k: result ) - assert item is state.item - assert approve is True - - -def test_approval_without_a_pending_run_is_rejected() -> None: - with pytest.raises(HTTPException) as exc_info: - human_in_the_loop_approval._resolve_approval( - None, - {"approval": {"call_id": "call_1", "approve": True}}, - ) + seen: dict[str, Any] = {} + + async def to_agui(events: Any, body: Any, **kwargs: Any): + seen["replayed"] = [event async for event in _drain(events, seen)] + yield SimpleNamespace(type="RUN_ERROR") + + async def _drain(events: Any, sink: dict[str, Any]): + try: + async for event in events: + yield event + except RuntimeError as exc: + sink["raised"] = exc + + monkeypatch.setattr( + translator_server, + "translator", + SimpleNamespace( + to_openai=lambda run_input: SimpleNamespace( + messages=[], tools=[], context=[] + ), + to_agui=to_agui, + ), + ) - assert exc_info.value.status_code == 409 + async def collect() -> list[Any]: + return [ + chunk + async for chunk in translator_server._stream_approval( + SimpleNamespace(agent=SimpleNamespace(tools=[])), + _run_input(), + None, + None, + False, + ) + ] + + asyncio.run(collect()) + + assert seen["replayed"] == ["event_1"], "partial output must survive the failure" + assert seen["raised"] is boom, "the error must reach to_agui, not be swallowed" def test_direct_server_forwards_context_to_the_sdk(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py index 9fd6bb1daf..e8e75ebf37 100644 --- a/integrations/openai-agents/python/examples/translator_server.py +++ b/integrations/openai-agents/python/examples/translator_server.py @@ -70,8 +70,9 @@ from ag_ui.core import CustomEvent, EventType, RunAgentInput from ag_ui.encoder import EventEncoder from ag_ui_openai_agents import AGUITranslator +from ag_ui_openai_agents.engine import ClientToolPending from agents_examples import DemoConfig, build_registry -from agents_examples.human_in_the_loop_approval import _resolve_approval +from agents_examples.human_in_the_loop_approval import resolve_approval logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -120,8 +121,9 @@ async def run(agent_name: str, body: RunAgentInput) -> StreamingResponse: body.run_id, ) if agent_name == "human_in_the_loop_approval": - pending_state = _PENDING_APPROVALS.get(body.thread_id) - item, approve = _resolve_approval(pending_state, body.forwarded_props) + pending_state, item, approve = resolve_approval( + _PENDING_APPROVALS, body.thread_id, body.forwarded_props + ) return StreamingResponse( _stream_approval(demo, body, pending_state, item, approve), media_type=encoder.get_content_type(), @@ -134,7 +136,7 @@ async def _stream_approval( body: RunAgentInput, pending_state: Any, item: Any, - approve: bool | None, + approve: bool, ): """Same shape as _stream, plus resuming from result.interruptions. @@ -149,7 +151,6 @@ async def _stream_approval( pending_state.approve(item) else: pending_state.reject(item) - del _PENDING_APPROVALS[body.thread_id] result = Runner.run_streamed(agent, pending_state) else: translated = translator.to_openai(body) @@ -168,14 +169,23 @@ async def _stream_approval( # straight to to_agui: end_custom_event has to already exist by the # time we call it, and interruptions aren't known until the drain # finishes. + # Collect as we go rather than in one comprehension: whatever streamed + # before a mid-drain stop still has to reach the client. A client-owned tool + # ends the run cleanly here; a real failure is kept and re-raised from + # _replay() below, so to_agui sees it and handles it the same way it would + # on any other route. + raw_events = [] + stream_error = None try: - raw_events = [event async for event in result.stream_events()] - except Exception: - logger.exception("Agent run failed") - return + async for event in result.stream_events(): + raw_events.append(event) + except ClientToolPending: + pass + except Exception as exc: + stream_error = exc end_custom_event = None - if result.interruptions: + if stream_error is None and result.interruptions: _PENDING_APPROVALS[body.thread_id] = result.to_state() end_custom_event = CustomEvent( type=EventType.CUSTOM, @@ -193,6 +203,8 @@ async def _stream_approval( async def _replay(): for event in raw_events: yield event + if stream_error is not None: + raise stream_error try: async for ag_event in translator.to_agui( From 83678b1fde1f8a3adc3500a2f4611a2e3ae849df Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Fri, 17 Jul 2026 07:43:31 +0200 Subject: [PATCH 66/94] fix(openai-agents): preserve schemas and clean up runs - Preserve client tool fields when JSON Schema omits its top-level type. - Finalize open AG-UI windows on errors and cancel owned SDK runs when stream consumption stops early. --- integrations/openai-agents/python/README.md | 40 ++++ .../engine/agui_to_openai.py | 9 +- .../src/ag_ui_openai_agents/translator.py | 183 ++++++++++-------- .../python/tests/test_translator.py | 160 +++++++++++++++ 4 files changed, 303 insertions(+), 89 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index e6127e1b8c..4c86bab970 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -454,6 +454,10 @@ if translated_input.tools: run_agent = agent.clone(tools=[*agent.tools, *translated_input.tools]) ``` +A tool's `parameters` pass through as its JSON Schema. `"type": "object"` is +optional there — if it's missing it gets added, and the declared fields are +kept as-is. Only a `None`/empty spec becomes an empty (parameter-less) schema. + ### Lifecycle Events `to_agui` always wraps the stream: `RUN_STARTED` is the first event @@ -492,6 +496,42 @@ Pass `emit_run_error=False` only if you emit your own terminal error event in an outer handler — otherwise the exception re-raises with no `RUN_ERROR` and the client just watches the stream stop. +Open windows are closed before the error goes out: a run that dies while an +assistant message is streaming still gets its `TEXT_MESSAGE_END` (and any +open tool-call, reasoning, or step end) ahead of `RUN_ERROR`, so a client +that keys its teardown on the `*_END` events doesn't leave the half-streamed +message spinning forever. + +### Client Disconnects + +If the client goes away, the server stops iterating `to_agui` and `to_agui` +cancels the SDK run for you (`RunResultStreaming.cancel()`) — the run is a +separate task, and left alone it keeps calling the model and running tools for +a client that will never see the result. Any exit before `RUN_FINISHED` +triggers it: the generator being closed, the request task being cancelled +(even while a state source is resolving), or a stream error — not just a +disconnect mid-token-stream. + +This only applies when you hand `to_agui` the `RunResultStreaming` object: + +```python +result = Runner.run_streamed(agent, input=translated_input.messages) +async for event in translator.to_agui(result, run_input): # cancelled on disconnect + yield encoder.encode(event) +``` + +Pass a bare `stream_events()` iterator instead and the run stays yours to +cancel — `to_agui` won't end something it wasn't given: + +```python +result = Runner.run_streamed(agent, input=translated_input.messages) +try: + async for event in translator.to_agui(result.stream_events(), run_input): + yield encoder.encode(event) +finally: + result.cancel() # your iterator, your cleanup +``` + ### Messages Snapshot `MESSAGES_SNAPSHOT` lets the client resync its whole message list in one diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index 28b2a9bb0f..d80b27d7dc 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -591,10 +591,11 @@ def translate_binary_content( def _ensure_object_schema(parameters: Any) -> dict[str, Any]: """Normalize a possibly-empty tool parameter spec into a JSON Schema object. - The Responses API requires every function tool's schema to be a + The OpenAI Agents SDK expects every function tool's schema to be a JSON Schema object. AG-UI tools sometimes ship parameter-less specs as None or {}; those get coerced to an empty-but-valid - object. + object. A spec that declares real fields but omits the optional + top-level "type" keeps its fields — only the missing key is added. Args: parameters: The tool's raw parameter spec. @@ -602,8 +603,10 @@ def _ensure_object_schema(parameters: Any) -> dict[str, Any]: Returns: A valid JSON Schema object. """ - if not isinstance(parameters, dict) or "type" not in parameters: + if not isinstance(parameters, dict) or not parameters: return {"type": "object", "properties": {}, "additionalProperties": True} + if "type" not in parameters: + return {**parameters, "type": "object"} return parameters @staticmethod diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index dbc26ab871..22031433e3 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -118,99 +118,110 @@ async def to_agui( TypeError: start_custom_event or end_custom_event was given but is not a CustomEvent instance. """ - # validate custom events - if start_custom_event and not isinstance(start_custom_event, CustomEvent): - raise TypeError(f"start_custom_event must be a CustomEvent, got {type(start_custom_event).__name__}") - if end_custom_event and not isinstance(end_custom_event, CustomEvent): - raise TypeError(f"end_custom_event must be a CustomEvent, got {type(end_custom_event).__name__}") - - # 1. RUN_STARTED — always first - yield RunStartedEvent( - type=EventType.RUN_STARTED, - thread_id=run_input.thread_id, - run_id=run_input.run_id, - # Read defensively — older RunAgentInput versions lack the field. - parent_run_id=getattr(run_input, "parent_run_id", None), - ) - - # 2. start_custom_event (optional) - if start_custom_event: - yield start_custom_event - - # 3. STATE_SNAPSHOT (initial) — resolve now ({} emits, None skips) - if initial_state is not None: - snapshot = await self._resolve_state_snapshot(initial_state) - if snapshot is not None: - yield StateSnapshotEvent( - type=EventType.STATE_SNAPSHOT, - snapshot=snapshot, - ) - - # Accept either the SDK result or an iterator already obtained from it. - stream_events = ( - events.stream_events() - if isinstance(events, RunResultStreaming) - else events - ) - outbound = self._outbound_cls() + lifecycle_completed = False try: - # Streamed STEP / TEXT / TOOL / REASONING events. - async for openai_event in stream_events: - for event in outbound.translate(openai_event): - yield event - # 4. Close any text, tool, reasoning, or step window still open. - for event in outbound.finalize(): - yield event - # 5. STATE_SNAPSHOT (final, optional) - if final_state is not None: - snapshot = await self._resolve_state_snapshot(final_state) + # The SDK run is already active, so validation stays in cleanup scope. + if start_custom_event is not None and not isinstance(start_custom_event, CustomEvent): + raise TypeError(f"start_custom_event must be a CustomEvent, got {type(start_custom_event).__name__}") + if end_custom_event is not None and not isinstance(end_custom_event, CustomEvent): + raise TypeError(f"end_custom_event must be a CustomEvent, got {type(end_custom_event).__name__}") + + # 1. RUN_STARTED — always first + yield RunStartedEvent( + type=EventType.RUN_STARTED, + thread_id=run_input.thread_id, + run_id=run_input.run_id, + # Read defensively — older RunAgentInput versions lack the field. + parent_run_id=getattr(run_input, "parent_run_id", None), + ) + + # 2. start_custom_event (optional) + if start_custom_event is not None: + yield start_custom_event + + # 3. STATE_SNAPSHOT (initial) — resolve now ({} emits, None skips) + if initial_state is not None: + snapshot = await self._resolve_state_snapshot(initial_state) if snapshot is not None: yield StateSnapshotEvent( type=EventType.STATE_SNAPSHOT, snapshot=snapshot, ) - # 6. MESSAGES_SNAPSHOT (optional) - if emit_messages_snapshot: - yield outbound.build_messages_snapshot(run_input) - except ClientToolPending: - # A client-owned tool ends this server run cleanly; its call events - # have already streamed to the frontend. Finish with steps 4–6. - # 4. Close any text, tool, reasoning, or step window still open. - for event in outbound.finalize(): - yield event - # 5. STATE_SNAPSHOT (final, optional) - if final_state is not None: - snapshot = await self._resolve_state_snapshot(final_state) - if snapshot is not None: - yield StateSnapshotEvent( - type=EventType.STATE_SNAPSHOT, - snapshot=snapshot, + + # Accept either the SDK result or an iterator already obtained from it. + stream_events = ( + events.stream_events() + if isinstance(events, RunResultStreaming) + else events + ) + outbound = self._outbound_cls() + try: + # Streamed STEP / TEXT / TOOL / REASONING events. + async for openai_event in stream_events: + for event in outbound.translate(openai_event): + yield event + # 4. Close any text, tool, reasoning, or step window still open. + for event in outbound.finalize(): + yield event + # 5. STATE_SNAPSHOT (final, optional) + if final_state is not None: + snapshot = await self._resolve_state_snapshot(final_state) + if snapshot is not None: + yield StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=snapshot, + ) + # 6. MESSAGES_SNAPSHOT (optional) + if emit_messages_snapshot: + yield outbound.build_messages_snapshot(run_input) + except ClientToolPending: + # A client-owned tool ends this server run cleanly; its call + # events have already streamed to the frontend. Finish with + # steps 4–6. + # 4. Close any text, tool, reasoning, or step window still open. + for event in outbound.finalize(): + yield event + # 5. STATE_SNAPSHOT (final, optional) + if final_state is not None: + snapshot = await self._resolve_state_snapshot(final_state) + if snapshot is not None: + yield StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=snapshot, + ) + # 6. MESSAGES_SNAPSHOT (optional) + if emit_messages_snapshot: + yield outbound.build_messages_snapshot(run_input) + except (Exception, asyncio.CancelledError) as exc: + # CancelledError is not an Exception; both paths must close open + # protocol windows before emitting the terminal error. + for event in outbound.finalize(): + yield event + if emit_run_error: + yield RunErrorEvent( + type=EventType.RUN_ERROR, + message=run_error_message or str(exc), ) - # 6. MESSAGES_SNAPSHOT (optional) - if emit_messages_snapshot: - yield outbound.build_messages_snapshot(run_input) - except (Exception, asyncio.CancelledError) as exc: - # asyncio.CancelledError is BaseException, not Exception (3.8+) — - # a mid-stream cancellation (timeout, dropped connection) would - # otherwise skip this handler entirely and the client would see - # the stream just stop, with no RUN_ERROR and no RUN_FINISHED. - if emit_run_error: - yield RunErrorEvent( - type=EventType.RUN_ERROR, - message=run_error_message or str(exc), - ) - raise - - # 7. end_custom_event (optional) - if end_custom_event: - yield end_custom_event - - # 8. RUN_FINISHED — always last - yield RunFinishedEvent( - type=EventType.RUN_FINISHED, - thread_id=run_input.thread_id, - run_id=run_input.run_id, - ) + raise + + # 7. end_custom_event (optional) + if end_custom_event is not None: + yield end_custom_event + + # 8. RUN_FINISHED — always last + run_finished_event = RunFinishedEvent( + type=EventType.RUN_FINISHED, + thread_id=run_input.thread_id, + run_id=run_input.run_id, + ) + # Set before yield because a consumer may close at the terminal event. + lifecycle_completed = True + yield run_finished_event + finally: + # Cancel an owned SDK result if AG-UI consumption stops early; + # callers retain ownership when they pass a bare iterator. + if not lifecycle_completed and isinstance(events, RunResultStreaming): + events.cancel() async def _resolve_state_snapshot(self, state: Any) -> Any: """Resolve a static, synchronous, or asynchronous state source.""" diff --git a/integrations/openai-agents/python/tests/test_translator.py b/integrations/openai-agents/python/tests/test_translator.py index 00163fd4a0..7b1ac6eb37 100644 --- a/integrations/openai-agents/python/tests/test_translator.py +++ b/integrations/openai-agents/python/tests/test_translator.py @@ -111,6 +111,26 @@ def test_to_openai_translates_messages_and_tools(): assert bundle.tools[0].strict_json_schema is False +def test_to_openai_keeps_a_tool_schema_that_omits_the_optional_type_key(): + # "type": "object" is optional in JSON Schema; dropping the declared + # fields with it would leave the model calling the tool with no arguments. + run_input = _run_input() + run_input.tools = [ + Tool( + name="confirm", + description="Ask the user to confirm.", + parameters={ + "properties": {"reason": {"type": "string"}}, + "required": ["reason"], + }, + ) + ] + schema = AGUITranslator().to_openai(run_input).tools[0].params_json_schema + assert schema["type"] == "object" + assert schema["properties"] == {"reason": {"type": "string"}} + assert schema["required"] == ["reason"] + + def test_to_openai_without_tools_leaves_bundle_empty(): bundle = AGUITranslator().to_openai(_run_input()) assert bundle.tools == [] @@ -334,6 +354,146 @@ async def collect(): assert error.message == "Agent run failed" +def test_to_agui_closes_open_windows_before_run_error(): + # A client keying its teardown on *_END would spin forever on the + # half-streamed message if the error path skipped the flush. + class _ExplodingOutbound(_StubOutbound): + def translate(self, openai_event): + raise RuntimeError("boom") + + translator = AGUITranslator(outbound_cls=_ExplodingOutbound) + collected: list = [] + + async def collect(): + async for e in translator.to_agui(_fake_stream("a"), _run_input()): + collected.append(e) + + with pytest.raises(RuntimeError, match="boom"): + asyncio.run(collect()) + + assert [getattr(e, "name", None) for e in collected if isinstance(e, CustomEvent)] == [ + "finalized" + ] + assert isinstance(collected[-1], RunErrorEvent) + + +@pytest.mark.parametrize( + ("custom_event_arg", "invalid_value"), + [ + ("start_custom_event", "not-a-custom-event"), + ("end_custom_event", False), + ], +) +def test_to_agui_cancels_the_sdk_run_when_custom_event_validation_fails( + custom_event_arg, invalid_value +): + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + + async def collect(): + async for _ in translator.to_agui( + result, + _run_input(), + **{custom_event_arg: invalid_value}, + ): + pass + + with pytest.raises(TypeError, match=f"{custom_event_arg} must be a CustomEvent"): + asyncio.run(collect()) + + result.cancel.assert_called_once() + + +@pytest.mark.parametrize( + ("events_to_read", "closed_at"), + [ + (1, "RUN_STARTED"), + (2, "the initial STATE_SNAPSHOT"), + (3, "the first streamed event"), + ], +) +def test_to_agui_cancels_the_sdk_run_when_the_consumer_stops_reading( + events_to_read, closed_at +): + # A disconnected client closes this generator wherever it happens to be + # paused; without the cancel the SDK run keeps calling the model and + # running tools for nobody, whichever yield that was. + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream("a", "b") + result.new_items = [] + + async def stop_after(event_count: int): + stream = translator.to_agui(result, _run_input(), initial_state={"k": "v"}) + for _ in range(event_count): + await stream.__anext__() + await stream.aclose() + + asyncio.run(stop_after(events_to_read)) + assert result.cancel.call_count == 1, f"closing at {closed_at} must cancel the run" + + +def test_to_agui_cancels_the_sdk_run_when_a_pending_step_is_cancelled(): + # Task cancellation raises CancelledError at an await, not GeneratorExit + # at a yield — here while resolving an async initial_state source, before + # the stream section's own handler is even in play. The run must still be + # cancelled. + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream("a") + result.new_items = [] + + async def never_resolves(): + await asyncio.Event().wait() + + async def cancel_mid_initial_state(): + stream = translator.to_agui( + result, _run_input(), initial_state=never_resolves + ) + await stream.__anext__() # RUN_STARTED + pending = asyncio.create_task(stream.__anext__()) + await asyncio.sleep(0) # let it reach the await inside initial_state + pending.cancel() + with pytest.raises(asyncio.CancelledError): + await pending + + asyncio.run(cancel_mid_initial_state()) + result.cancel.assert_called_once() + + +def test_to_agui_does_not_cancel_a_run_that_streamed_to_completion(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream("a") + result.new_items = [] + + async def collect(): + return [e async for e in translator.to_agui(result, _run_input())] + + asyncio.run(collect()) + result.cancel.assert_not_called() + + +def test_to_agui_does_not_cancel_when_closed_right_after_run_finished(): + # A generator pauses at yield, so the consumer holds RUN_FINISHED while + # to_agui is still suspended on that line. Closing there is a normal end, + # not an interruption — the run finished, nothing is left to cancel. + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream("a") + result.new_items = [] + + async def close_on_run_finished(): + stream = translator.to_agui(result, _run_input()) + async for event in stream: + if isinstance(event, RunFinishedEvent): + break # implicit aclose while paused at the terminal yield + await stream.aclose() + + asyncio.run(close_on_run_finished()) + result.cancel.assert_not_called() + + def test_to_agui_emits_run_error_on_cancelled_error(): # asyncio.CancelledError is BaseException, not Exception (3.8+) — a # mid-stream timeout/dropped-connection must still surface RUN_ERROR From a30d4dc8465d4eec2d6139dfce1f4dc3cd27d7e2 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 00:37:09 +0200 Subject: [PATCH 67/94] feat(openai-agents): harden wrapper setup - Resolve lifecycle event factories before SDK runs start so factory failures cannot leave background work running. - Make health registration optional for host applications that own the route, and clarify wrapper and endpoint contracts with focused coverage. --- integrations/openai-agents/python/README.md | 5 +- .../python/src/ag_ui_openai_agents/agent.py | 49 +++++++------------ .../src/ag_ui_openai_agents/endpoint.py | 31 +++++++----- .../openai-agents/python/tests/test_agent.py | 19 +++++++ .../python/tests/test_endpoint.py | 16 ++++++ 5 files changed, 76 insertions(+), 44 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 4c86bab970..35b1a82348 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -365,7 +365,7 @@ another request. `add_openai_agents_fastapi_endpoint` connects an `OpenAIAgentsAgent` to a FastAPI app. It is the highest-level option: it owns HTTP POST handling, AG-UI -SSE encoding, and a small health endpoint, but not your agent's own behavior. +SSE encoding, and an optional health endpoint, but not your agent's own behavior. ```python app = FastAPI() @@ -377,6 +377,7 @@ add_openai_agents_fastapi_endpoint(app, wrapped_agent, "/assistant") | `app` | required | FastAPI application to register routes on. | | `agent` | required | `OpenAIAgentsAgent` to execute for incoming requests. | | `path` | `"/"` | POST route that accepts `RunAgentInput`. | +| `include_health` | `True` | Whether to register `GET {path}/health`. | For `path="/assistant"`, the helper registers: @@ -385,6 +386,8 @@ For `path="/assistant"`, the helper registers: | `POST /assistant` | Runs the wrapper and returns encoded AG-UI SSE events. | | `GET /assistant/health` | Returns `{"status": "ok", "agent": {"name": ...}}`. | +Pass `include_health=False` when the application owns its health route. + The helper uses the request `Accept` header when creating `EventEncoder`, so its response content type matches AG-UI content negotiation. It adds routes only; it does not start a server or manage sessions. diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index b1bcb51d79..ec63973633 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -1,8 +1,4 @@ -"""Agent wrapper — an OpenAI Agents SDK Agent that speaks AG-UI. - -Wrap a plain SDK Agent, get one run_streamed(RunAgentInput) yielding AG-UI events, -ready for add_openai_agents_fastapi_endpoint. -""" +"""Agent wrapper — an OpenAI Agents SDK Agent that speaks AG-UI.""" from typing import Any, AsyncIterator, Callable @@ -15,12 +11,8 @@ class OpenAIAgentsAgent: """Wrap an OpenAI Agents SDK Agent so it speaks the AG-UI protocol. - One instance per server process is fine. The wrapper holds no per-request - state: an SDK Agent is a config template that Runner.run_streamed never - mutates, and AGUITranslator is stateless on the inbound side and spins up a - fresh outbound engine per to_agui call. Client-declared tools arriving on a - request are merged onto a per-request agent.clone(), so concurrent requests - never see each other's tools. + The wrapper is reusable across requests. Client-declared tools are added to + a per-request agent clone, and output translation uses fresh per-run state. Example: from agents import Agent @@ -49,17 +41,16 @@ def __init__( emit_run_error: bool = True, run_error_message: str | None = None, ) -> None: - """Wrap OpenAI Agents SDK Agent. + """Configure an OpenAI Agents SDK Agent for AG-UI requests. Args: agent: The OpenAI Agents SDK Agent to serve. name: Public name for health/introspection. Defaults to agent.name. description: Optional human-readable description. - translator: An AGUITranslator to reuse. Defaults to a fresh one; - pass your own to inject engine subclasses (per-mapping overrides). - run_config: Passed straight to Runner.run_streamed on every run — - the place to set run-wide model settings. None uses the SDK - defaults (native OpenAI). + translator: Reusable translator. Provide one to customize mapping + through engine subclasses. + run_config: Run-wide OpenAI Agents SDK configuration. None uses the + SDK defaults. start_custom_event: A CustomEvent, or a zero-argument factory that builds one per run, emitted after RUN_STARTED. initial_state: Passed to AGUITranslator.to_agui. Accepts the same @@ -88,14 +79,10 @@ def __init__( self._run_error_message = run_error_message async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: - """Run the agent for one AG-UI request and yield AG-UI events. + """Translate one AG-UI request, run the agent, and yield AG-UI events. - Orchestration only — the translator does the mapping. to_openai turns the - request into SDK input items plus FunctionTool proxies for any - client-declared tools; those proxies are merged onto a per-request - clone so the static agent stays untouched. to_agui wraps the SDK stream - with the lifecycle events (RUN_STARTED/FINISHED/ERROR), the STATE and - MESSAGES snapshots, and the flush — nothing to hand-roll here. + Client tools are added to a per-request agent clone. The translator + handles input and output mapping, lifecycle events, and snapshots. Args: input: The incoming AG-UI RunAgentInput. @@ -105,20 +92,22 @@ async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: """ translated = self._translator.to_openai(input) - agent = self.agent + run_agent = self.agent if translated.tools: - agent = agent.clone(tools=[*self.agent.tools, *translated.tools]) + run_agent = self.agent.clone( + tools=[*self.agent.tools, *translated.tools] + ) + + start_custom_event = self._resolve_custom_event(self._start_custom_event) + end_custom_event = self._resolve_custom_event(self._end_custom_event) result = Runner.run_streamed( - agent, + run_agent, input=translated.messages, run_config=self._run_config, context=translated.context, ) - start_custom_event = self._resolve_custom_event(self._start_custom_event) - end_custom_event = self._resolve_custom_event(self._end_custom_event) - async for event in self._translator.to_agui( result, input, diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py index f33ce4b905..cbd0653b86 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py @@ -1,10 +1,7 @@ -"""FastAPI endpoint helper for the OpenAI Agents SDK integration. - -Glue an OpenAIAgentsAgent to a FastAPI app in one call: SSE stream, content -negotiation, health check. -""" +"""FastAPI endpoint helper for serving an OpenAI Agents SDK agent over AG-UI.""" import logging +from typing import AsyncIterator from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse @@ -20,18 +17,24 @@ def add_openai_agents_fastapi_endpoint( app: FastAPI, agent: OpenAIAgentsAgent, path: str = "/", + *, + include_health: bool = True, ) -> None: """Add an OpenAI Agents SDK agent endpoint to a FastAPI app. Args: app: The FastAPI application to register the route on. agent: The wrapped agent to serve. - path: The POST path for the agent. Defaults to "/". A GET health check - is registered alongside it. + path: The POST path for the agent. Defaults to "/". + include_health: Whether to register a GET health check alongside the + agent endpoint. Defaults to True. """ @app.post(path) - async def openai_agents_endpoint(input_data: RunAgentInput, request: Request): + async def openai_agents_endpoint( + input_data: RunAgentInput, + request: Request, + ) -> StreamingResponse: """Run the agent and stream AG-UI events back to the client.""" logger.info( "Starting agent run: agent=%s thread_id=%s run_id=%s", @@ -42,7 +45,7 @@ async def openai_agents_endpoint(input_data: RunAgentInput, request: Request): accept_header = request.headers.get("accept") encoder = EventEncoder(accept=accept_header) - async def event_generator(): + async def event_generator() -> AsyncIterator[str]: async for event in agent.run_streamed(input_data): yield encoder.encode(event) @@ -51,7 +54,9 @@ async def event_generator(): media_type=encoder.get_content_type(), ) - @app.get(path.rstrip("/") + "/health") - def health(): - """Health check.""" - return {"status": "ok", "agent": {"name": agent.name}} + if include_health: + + @app.get(path.rstrip("/") + "/health") + def health(): + """Health check.""" + return {"status": "ok", "agent": {"name": agent.name}} diff --git a/integrations/openai-agents/python/tests/test_agent.py b/integrations/openai-agents/python/tests/test_agent.py index 023c464c6d..3b8ecb6347 100644 --- a/integrations/openai-agents/python/tests/test_agent.py +++ b/integrations/openai-agents/python/tests/test_agent.py @@ -186,6 +186,25 @@ def end_custom_event(): assert calls == {"start": 2, "end": 2} +@pytest.mark.parametrize("custom_event_arg", ["start_custom_event", "end_custom_event"]) +def test_custom_event_factory_failure_does_not_start_run( + patched_runner, + custom_event_arg, +): + def failing_factory(): + raise RuntimeError("event factory failed") + + wrapper = OpenAIAgentsAgent( + Agent(name="assistant", instructions="hi"), + **{custom_event_arg: failing_factory}, + ) + + with pytest.raises(RuntimeError, match="event factory failed"): + _collect(wrapper, _run_input()) + + assert patched_runner == [] + + def test_name_defaults_to_agent_name(): assert OpenAIAgentsAgent(Agent(name="helper", instructions="hi")).name == "helper" assert ( diff --git a/integrations/openai-agents/python/tests/test_endpoint.py b/integrations/openai-agents/python/tests/test_endpoint.py index 59be90b6ad..0503dc4600 100644 --- a/integrations/openai-agents/python/tests/test_endpoint.py +++ b/integrations/openai-agents/python/tests/test_endpoint.py @@ -6,6 +6,7 @@ - POST on the given path streams the run as SSE frames. - A GET health check is registered next to it and reports the agent name. - Custom (non-root) paths get their health check at /health. +- Health registration can be disabled when the application owns that route. """ from __future__ import annotations @@ -84,3 +85,18 @@ def fake_run_streamed(agent, *, input, run_config=None, **kwargs): assert client.get("/my_agent/health").status_code == 200 with client.stream("POST", "/my_agent", json=RUN_INPUT_JSON) as response: assert response.status_code == 200 + + +def test_health_can_be_disabled(): + app = FastAPI() + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi")) + add_openai_agents_fastapi_endpoint( + app, + wrapper, + "/my_agent", + include_health=False, + ) + + routes = {(route.path, method) for route in app.routes for method in route.methods} + assert ("/my_agent", "POST") in routes + assert ("/my_agent/health", "GET") not in routes From b02decb9547a72038fc76d749c670ead9f86f940 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 06:31:30 +0200 Subject: [PATCH 68/94] feat(openai-agents): refine lifecycle handling - resolve dynamic custom-event values at emission time - centralize lifecycle error handling and abandoned-run cancellation - document wrapper and endpoint behavior with expanded tests --- integrations/openai-agents/python/README.md | 14 +- .../python/src/ag_ui_openai_agents/agent.py | 42 +++-- .../src/ag_ui_openai_agents/engine/types.py | 6 +- .../src/ag_ui_openai_agents/translator.py | 117 +++++++------- .../openai-agents/python/tests/test_agent.py | 81 +++++++--- .../python/tests/test_endpoint.py | 28 ++++ .../python/tests/test_translator.py | 148 +++++++++++++++++- 7 files changed, 316 insertions(+), 120 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 35b1a82348..f521124f65 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -294,19 +294,19 @@ the lifecycle IDs and message history for snapshots. | Parameter | Default | Meaning | |---|---|---| -| `start_custom_event` | `None` | A `CustomEvent` sent after `RUN_STARTED`. | +| `start_custom_event` | `None` | A `CustomEvent` sent after `RUN_STARTED`; its value may be static or a sync/async factory. | | `initial_state` | `None` | Static, sync, or async source for an initial `STATE_SNAPSHOT`. | | `final_state` | `None` | Static, sync, or async source for a final `STATE_SNAPSHOT`. | | `emit_messages_snapshot` | `True` | Add `MESSAGES_SNAPSHOT` before the terminal event. | -| `end_custom_event` | `None` | A `CustomEvent` sent after the snapshots and before `RUN_FINISHED`. | +| `end_custom_event` | `None` | A `CustomEvent` sent after the snapshots and before `RUN_FINISHED`; its value may be static or a sync/async factory. | | `emit_run_error` | `True` | Send `RUN_ERROR` if streaming raises, then re-raise the original exception. | | `run_error_message` | `None` | Client-safe error text; by default the event uses `str(exception)`. | `initial_state` and `final_state` can each be a value, a zero-argument function, or a zero-argument async function. A supplied `None` skips that -snapshot; an empty `{}` is still a valid snapshot. Custom-event values must be -`CustomEvent` objects. The wrapper supports factories for these events; the -direct translator intentionally accepts the event object for this one run. +snapshot; an empty `{}` is still a valid snapshot. Custom events are passed as +complete `CustomEvent` objects. Their `value` may use the same static, sync, or +async forms and is resolved immediately before the event is emitted. Successful runs follow this order: @@ -347,11 +347,11 @@ wrapped_agent = OpenAIAgentsAgent( | `description` | `""` | Optional application metadata. | | `translator` | new `AGUITranslator` | Translator instance, including any custom engine classes. | | `run_config` | `None` | `RunConfig` passed to every `Runner.run_streamed` call. | -| `start_custom_event` | `None` | A `CustomEvent` or zero-argument factory, emitted after `RUN_STARTED`. | +| `start_custom_event` | `None` | A `CustomEvent` whose static or dynamic value is resolved after `RUN_STARTED`. | | `initial_state` | `None` | Same state-source forms as `AGUITranslator.to_agui`. | | `final_state` | `None` | Same state-source forms as `AGUITranslator.to_agui`. | | `emit_messages_snapshot` | `True` | Forwarded to `to_agui`. | -| `end_custom_event` | `None` | A `CustomEvent` or zero-argument factory, emitted before the terminal event. | +| `end_custom_event` | `None` | A `CustomEvent` whose static or dynamic value is resolved before the terminal event. | | `emit_run_error` | `True` | Forwarded to `to_agui`. | | `run_error_message` | `None` | Forwarded to `to_agui`. | diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index ec63973633..edfed6f31c 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -1,6 +1,6 @@ """Agent wrapper — an OpenAI Agents SDK Agent that speaks AG-UI.""" -from typing import Any, AsyncIterator, Callable +from typing import Any, AsyncIterator from agents import Agent, RunConfig, Runner @@ -14,6 +14,13 @@ class OpenAIAgentsAgent: The wrapper is reusable across requests. Client-declared tools are added to a per-request agent clone, and output translation uses fresh per-run state. + This is the opinionated shortcut: it hides the run loop (to_openai, + Runner.run_streamed, to_agui) behind one call. If you need the run itself — + real token usage, interruptions, anything off the RunResultStreaming — use + the translator directly instead (see AGUITranslator); it hands you the + result so you can close over it. The wrapper deliberately does not grow a + parameter per such need. + Example: from agents import Agent from ag_ui_openai_agents import OpenAIAgentsAgent @@ -33,11 +40,11 @@ def __init__( description: str = "", translator: AGUITranslator | None = None, run_config: RunConfig | None = None, - start_custom_event: CustomEvent | Callable[[], CustomEvent] | None = None, + start_custom_event: CustomEvent | None = None, initial_state: Any = None, final_state: Any = None, emit_messages_snapshot: bool = True, - end_custom_event: CustomEvent | Callable[[], CustomEvent] | None = None, + end_custom_event: CustomEvent | None = None, emit_run_error: bool = True, run_error_message: str | None = None, ) -> None: @@ -51,16 +58,15 @@ def __init__( through engine subclasses. run_config: Run-wide OpenAI Agents SDK configuration. None uses the SDK defaults. - start_custom_event: A CustomEvent, or a zero-argument factory that - builds one per run, emitted after RUN_STARTED. - initial_state: Passed to AGUITranslator.to_agui. Accepts the same - static, synchronous, or asynchronous state sources. - final_state: Passed to AGUITranslator.to_agui. Accepts the same - static, synchronous, or asynchronous state sources. + start_custom_event: CustomEvent emitted after RUN_STARTED. Its value + may be static or a zero-argument sync/async factory. + initial_state: Passed to AGUITranslator.to_agui unchanged. + final_state: Passed to AGUITranslator.to_agui unchanged. emit_messages_snapshot: Whether to emit MESSAGES_SNAPSHOT before RUN_FINISHED. Defaults to True. - end_custom_event: A CustomEvent, or a zero-argument factory that - builds one per run, emitted before the terminal lifecycle event. + end_custom_event: CustomEvent emitted before the terminal event. + Its value may be static or a zero-argument sync/async factory. + To read the run result, compose the translator directly. emit_run_error: Whether to emit RUN_ERROR when streaming fails. Defaults to True. run_error_message: Fixed RUN_ERROR message. None sends str(exc). @@ -98,9 +104,6 @@ async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: tools=[*self.agent.tools, *translated.tools] ) - start_custom_event = self._resolve_custom_event(self._start_custom_event) - end_custom_event = self._resolve_custom_event(self._end_custom_event) - result = Runner.run_streamed( run_agent, input=translated.messages, @@ -111,19 +114,12 @@ async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: async for event in self._translator.to_agui( result, input, - start_custom_event=start_custom_event, + start_custom_event=self._start_custom_event, initial_state=self._initial_state, final_state=self._final_state, emit_messages_snapshot=self._emit_messages_snapshot, - end_custom_event=end_custom_event, + end_custom_event=self._end_custom_event, emit_run_error=self._emit_run_error, run_error_message=self._run_error_message, ): yield event - - @staticmethod - def _resolve_custom_event( - source: CustomEvent | Callable[[], CustomEvent] | None, - ) -> CustomEvent | None: - """Resolve a fixed custom event or a per-run event factory.""" - return source() if callable(source) else source diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index f0fd9c3b33..cdb12cb2c6 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -177,10 +177,8 @@ class OpenAIItemType(str, Enum): class ClientToolPending(AgentsException): """Raised by a client-tool proxy to signal "stop, the UI owns this call". - The outer run loop catches it, cancels the SDK run after the current - turn, and persists the resulting RunState keyed by thread_id so the - next AG-UI request (which carries an AG-UI ToolMessage with the - client's result) can resume from the same point. + The AG-UI run closes normally after forwarding the tool call. The client + executes it and returns the result in a later request. Subclasses AgentsException deliberately: the SDK's own tool executor (agents.run_internal.tool_execution._run_single_tool) special-cases diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index 22031433e3..fa1e099b77 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -1,6 +1,5 @@ """Translate between AG-UI requests and OpenAI Agents SDK streams.""" -import asyncio import inspect from typing import Any, AsyncIterator @@ -104,10 +103,16 @@ async def to_agui( events: A ``RunResultStreaming`` or its ``stream_events()`` iterator. run_input: The request supplying lifecycle IDs and message history. start_custom_event: ``CustomEvent`` emitted after ``RUN_STARTED``. - initial_state: Optional static, sync, or async source for the first snapshot. - final_state: Optional static, sync, or async source for the final snapshot. + Its value may be static or a zero-argument sync/async factory. + initial_state: Optional source for the first snapshot. Static value + or zero-argument sync/async factory. + final_state: Optional source for the final snapshot. Static value or + zero-argument sync/async factory, resolved after the stream so it + can observe changes the run made. emit_messages_snapshot: Append ``MESSAGES_SNAPSHOT`` before finishing. end_custom_event: ``CustomEvent`` emitted before ``RUN_FINISHED``. + Its value may be static or a zero-argument sync/async factory + resolved just before emission. emit_run_error: Emit ``RUN_ERROR`` before re-raising a stream error. run_error_message: Safe fixed ``RUN_ERROR`` message; defaults to ``str(exc)``. @@ -115,17 +120,12 @@ async def to_agui( AG-UI events ready to encode or send. Raises: - TypeError: start_custom_event or end_custom_event was given but - is not a CustomEvent instance. + TypeError: A custom event is not a CustomEvent instance. """ + # Actual failures emit RUN_ERROR; cancellation only stops an owned SDK run. lifecycle_completed = False + outbound: OpenAIToAGUITranslator | None = None try: - # The SDK run is already active, so validation stays in cleanup scope. - if start_custom_event is not None and not isinstance(start_custom_event, CustomEvent): - raise TypeError(f"start_custom_event must be a CustomEvent, got {type(start_custom_event).__name__}") - if end_custom_event is not None and not isinstance(end_custom_event, CustomEvent): - raise TypeError(f"end_custom_event must be a CustomEvent, got {type(end_custom_event).__name__}") - # 1. RUN_STARTED — always first yield RunStartedEvent( type=EventType.RUN_STARTED, @@ -137,11 +137,11 @@ async def to_agui( # 2. start_custom_event (optional) if start_custom_event is not None: - yield start_custom_event + yield await self._resolve_custom_event(start_custom_event) # 3. STATE_SNAPSHOT (initial) — resolve now ({} emits, None skips) if initial_state is not None: - snapshot = await self._resolve_state_snapshot(initial_state) + snapshot = await self._resolve_lifecycle_value(initial_state) if snapshot is not None: yield StateSnapshotEvent( type=EventType.STATE_SNAPSHOT, @@ -160,53 +160,27 @@ async def to_agui( async for openai_event in stream_events: for event in outbound.translate(openai_event): yield event - # 4. Close any text, tool, reasoning, or step window still open. - for event in outbound.finalize(): - yield event - # 5. STATE_SNAPSHOT (final, optional) - if final_state is not None: - snapshot = await self._resolve_state_snapshot(final_state) - if snapshot is not None: - yield StateSnapshotEvent( - type=EventType.STATE_SNAPSHOT, - snapshot=snapshot, - ) - # 6. MESSAGES_SNAPSHOT (optional) - if emit_messages_snapshot: - yield outbound.build_messages_snapshot(run_input) except ClientToolPending: - # A client-owned tool ends this server run cleanly; its call - # events have already streamed to the frontend. Finish with - # steps 4–6. - # 4. Close any text, tool, reasoning, or step window still open. - for event in outbound.finalize(): - yield event - # 5. STATE_SNAPSHOT (final, optional) - if final_state is not None: - snapshot = await self._resolve_state_snapshot(final_state) - if snapshot is not None: - yield StateSnapshotEvent( - type=EventType.STATE_SNAPSHOT, - snapshot=snapshot, - ) - # 6. MESSAGES_SNAPSHOT (optional) - if emit_messages_snapshot: - yield outbound.build_messages_snapshot(run_input) - except (Exception, asyncio.CancelledError) as exc: - # CancelledError is not an Exception; both paths must close open - # protocol windows before emitting the terminal error. - for event in outbound.finalize(): - yield event - if emit_run_error: - yield RunErrorEvent( - type=EventType.RUN_ERROR, - message=run_error_message or str(exc), + # Finish client-owned tool calls through the shared steps below. + pass + + # 4. Close any text, tool, reasoning, or step window still open. + for event in outbound.finalize(): + yield event + # 5. STATE_SNAPSHOT (final, optional) — resolved after the stream. + if final_state is not None: + snapshot = await self._resolve_lifecycle_value(final_state) + if snapshot is not None: + yield StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=snapshot, ) - raise - - # 7. end_custom_event (optional) + # 6. MESSAGES_SNAPSHOT (optional) + if emit_messages_snapshot: + yield outbound.build_messages_snapshot(run_input) + # 7. end_custom_event (optional) — resolved just before emitting. if end_custom_event is not None: - yield end_custom_event + yield await self._resolve_custom_event(end_custom_event) # 8. RUN_FINISHED — always last run_finished_event = RunFinishedEvent( @@ -217,15 +191,38 @@ async def to_agui( # Set before yield because a consumer may close at the terminal event. lifecycle_completed = True yield run_finished_event + except Exception as exc: + if outbound is not None: + for event in outbound.finalize(): + yield event + if emit_run_error: + yield RunErrorEvent( + type=EventType.RUN_ERROR, + message=run_error_message or str(exc), + ) + raise finally: # Cancel an owned SDK result if AG-UI consumption stops early; # callers retain ownership when they pass a bare iterator. if not lifecycle_completed and isinstance(events, RunResultStreaming): events.cancel() - async def _resolve_state_snapshot(self, state: Any) -> Any: - """Resolve a static, synchronous, or asynchronous state source.""" - value = state() if callable(state) else state + async def _resolve_custom_event( + self, + event: CustomEvent, + ) -> CustomEvent: + """Return a custom event with its value resolved.""" + if not isinstance(event, CustomEvent): + raise TypeError( + f"custom event must be a CustomEvent, got {type(event).__name__}" + ) + value = await self._resolve_lifecycle_value(event.value) + return event.model_copy(update={"value": value}) + + @staticmethod + async def _resolve_lifecycle_value(source: Any) -> Any: + """Resolve a static value or a synchronous/asynchronous factory.""" + value = source() if callable(source) else source if inspect.isawaitable(value): value = await value return value diff --git a/integrations/openai-agents/python/tests/test_agent.py b/integrations/openai-agents/python/tests/test_agent.py index 3b8ecb6347..5b9e17b2cd 100644 --- a/integrations/openai-agents/python/tests/test_agent.py +++ b/integrations/openai-agents/python/tests/test_agent.py @@ -127,18 +127,20 @@ def test_context_passes_through(patched_runner): def test_to_agui_options_pass_through(patched_runner, monkeypatch): - start = CustomEvent(type=EventType.CUSTOM, name="start", value={}) - end = CustomEvent(type=EventType.CUSTOM, name="end", value={}) + start_value = lambda: {"phase": "start"} + end_value = lambda: {"phase": "end"} + start = CustomEvent(type=EventType.CUSTOM, name="start", value=start_value) + end = CustomEvent(type=EventType.CUSTOM, name="end", value=end_value) initial_state = lambda: {"phase": "initial"} final_state = lambda: {"phase": "final"} translated_calls: list[dict] = [] wrapper = OpenAIAgentsAgent( Agent(name="assistant", instructions="hi"), - start_custom_event=lambda: start, + start_custom_event=start, initial_state=initial_state, final_state=final_state, emit_messages_snapshot=False, - end_custom_event=lambda: end, + end_custom_event=end, emit_run_error=False, run_error_message="safe error", ) @@ -165,44 +167,85 @@ async def spy_to_agui(result, run_input, **kwargs): ] -def test_custom_event_factories_run_once_per_request(patched_runner): +def test_custom_event_value_factories_run_once_per_request(patched_runner): calls = {"start": 0, "end": 0} - def start_custom_event(): + def start_value(): calls["start"] += 1 - return CustomEvent(type=EventType.CUSTOM, name="start", value={}) + return {"call": calls["start"]} - def end_custom_event(): + def end_value(): calls["end"] += 1 - return CustomEvent(type=EventType.CUSTOM, name="end", value={}) + return {"call": calls["end"]} wrapper = OpenAIAgentsAgent( Agent(name="assistant", instructions="hi"), - start_custom_event=start_custom_event, - end_custom_event=end_custom_event, + start_custom_event=CustomEvent( + type=EventType.CUSTOM, name="start", value=start_value + ), + end_custom_event=CustomEvent( + type=EventType.CUSTOM, name="end", value=end_value + ), ) - _collect(wrapper, _run_input()) - _collect(wrapper, _run_input()) + first = _collect(wrapper, _run_input()) + second = _collect(wrapper, _run_input()) + assert calls == {"start": 2, "end": 2} + assert [event.value for event in first if isinstance(event, CustomEvent)] == [ + {"call": 1}, + {"call": 1}, + ] + assert [event.value for event in second if isinstance(event, CustomEvent)] == [ + {"call": 2}, + {"call": 2}, + ] @pytest.mark.parametrize("custom_event_arg", ["start_custom_event", "end_custom_event"]) -def test_custom_event_factory_failure_does_not_start_run( - patched_runner, +def test_custom_event_value_factory_failure_emits_run_error_and_cancels( + monkeypatch, custom_event_arg, ): def failing_factory(): raise RuntimeError("event factory failed") + captured: list = [] + collected: list = [] + + def fake_run_streamed(agent, *, input, run_config=None, **kwargs): + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _empty_stream() + result.new_items = [] + captured.append(result) + return result + wrapper = OpenAIAgentsAgent( Agent(name="assistant", instructions="hi"), - **{custom_event_arg: failing_factory}, + **{ + custom_event_arg: CustomEvent( + type=EventType.CUSTOM, + name="failing", + value=failing_factory, + ) + }, ) - with pytest.raises(RuntimeError, match="event factory failed"): - _collect(wrapper, _run_input()) + monkeypatch.setattr(agent_module.Runner, "run_streamed", fake_run_streamed) - assert patched_runner == [] + async def collect(): + async for event in wrapper.run_streamed(_run_input()): + collected.append(event) + + with pytest.raises(RuntimeError, match="event factory failed"): + asyncio.run(collect()) + + assert len(captured) == 1, "the run must have started" + captured[0].cancel.assert_called_once() + assert [ + event.type + for event in collected + if event.type in {EventType.RUN_STARTED, EventType.RUN_ERROR} + ] == [EventType.RUN_STARTED, EventType.RUN_ERROR] def test_name_defaults_to_agent_name(): diff --git a/integrations/openai-agents/python/tests/test_endpoint.py b/integrations/openai-agents/python/tests/test_endpoint.py index 0503dc4600..0f1024bbb8 100644 --- a/integrations/openai-agents/python/tests/test_endpoint.py +++ b/integrations/openai-agents/python/tests/test_endpoint.py @@ -20,6 +20,7 @@ from fastapi.testclient import TestClient import ag_ui_openai_agents.agent as agent_module +from ag_ui.core import CustomEvent, EventType from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint RUN_INPUT_JSON = { @@ -63,6 +64,33 @@ def test_post_streams_sse_run(client): assert body.count("data: ") >= 2, "each event must be its own SSE frame" +def test_post_resolves_dynamic_custom_event_values(monkeypatch): + def fake_run_streamed(agent, *, input, run_config=None, **kwargs): + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _empty_stream() + return result + + monkeypatch.setattr(agent_module.Runner, "run_streamed", fake_run_streamed) + + app = FastAPI() + wrapper = OpenAIAgentsAgent( + Agent(name="assistant", instructions="hi"), + start_custom_event=CustomEvent( + type=EventType.CUSTOM, + name="request_metadata", + value=lambda: {"status": "dynamic"}, + ), + ) + add_openai_agents_fastapi_endpoint(app, wrapper, "/") + + with TestClient(app).stream("POST", "/", json=RUN_INPUT_JSON) as response: + body = "".join(response.iter_text()) + + assert response.status_code == 200 + assert '"name":"request_metadata"' in body + assert '"status":"dynamic"' in body + + def test_health_reports_agent_name(client): response = client.get("/health") assert response.status_code == 200 diff --git a/integrations/openai-agents/python/tests/test_translator.py b/integrations/openai-agents/python/tests/test_translator.py index 7b1ac6eb37..39dbb7ed71 100644 --- a/integrations/openai-agents/python/tests/test_translator.py +++ b/integrations/openai-agents/python/tests/test_translator.py @@ -377,6 +377,71 @@ async def collect(): assert isinstance(collected[-1], RunErrorEvent) +def test_to_agui_resolves_custom_event_values_at_their_lifecycle_positions(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + state = {"status": "starting"} + start = CustomEvent( + type=EventType.CUSTOM, + name="start", + value={"status": "starting"}, + ) + end = CustomEvent( + type=EventType.CUSTOM, + name="end", + value=lambda: dict(state), + ) + + async def stream(): + state["status"] = "complete" + yield "a" + + async def collect(): + return [ + event + async for event in translator.to_agui( + stream(), + _run_input(), + start_custom_event=start, + end_custom_event=end, + emit_messages_snapshot=False, + ) + ] + + events = asyncio.run(collect()) + lifecycle_events = [ + event for event in events if getattr(event, "name", None) in {"start", "end"} + ] + assert [event.value for event in lifecycle_events] == [ + {"status": "starting"}, + {"status": "complete"}, + ] + assert callable(end.value), "the reusable event must retain its factory" + + +def test_to_agui_awaits_an_async_custom_event_value_factory(): + translator = AGUITranslator(outbound_cls=_StubOutbound) + + async def end_value(): + return {"status": "complete"} + + end = CustomEvent(type=EventType.CUSTOM, name="end", value=end_value) + + async def collect(): + return [ + event + async for event in translator.to_agui( + _fake_stream(), + _run_input(), + end_custom_event=end, + emit_messages_snapshot=False, + ) + ] + + events = asyncio.run(collect()) + emitted = next(event for event in events if getattr(event, "name", None) == "end") + assert emitted.value == {"status": "complete"} + + @pytest.mark.parametrize( ("custom_event_arg", "invalid_value"), [ @@ -389,6 +454,7 @@ def test_to_agui_cancels_the_sdk_run_when_custom_event_validation_fails( ): translator = AGUITranslator(outbound_cls=_StubOutbound) result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream() async def collect(): async for _ in translator.to_agui( @@ -398,9 +464,77 @@ async def collect(): ): pass - with pytest.raises(TypeError, match=f"{custom_event_arg} must be a CustomEvent"): + with pytest.raises(TypeError, match="custom event must be a CustomEvent"): + asyncio.run(collect()) + + result.cancel.assert_called_once() + + +@pytest.mark.parametrize("custom_event_arg", ["start_custom_event", "end_custom_event"]) +def test_to_agui_emits_run_error_when_custom_event_value_factory_fails( + custom_event_arg, +): + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream() + collected: list = [] + + def failing_value(): + raise RuntimeError("value failed") + + async def collect(): + async for event in translator.to_agui( + result, + _run_input(), + emit_messages_snapshot=False, + **{ + custom_event_arg: CustomEvent( + type=EventType.CUSTOM, + name="failing", + value=failing_value, + ) + }, + ): + collected.append(event) + + with pytest.raises(RuntimeError, match="value failed"): asyncio.run(collect()) + assert [ + event.type + for event in collected + if event.type in {EventType.RUN_STARTED, EventType.RUN_ERROR} + ] == [EventType.RUN_STARTED, EventType.RUN_ERROR] + result.cancel.assert_called_once() + + +@pytest.mark.parametrize("state_arg", ["initial_state", "final_state"]) +def test_to_agui_emits_run_error_when_state_factory_fails(state_arg): + translator = AGUITranslator(outbound_cls=_StubOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream() + collected: list = [] + + def failing_state(): + raise RuntimeError("state failed") + + async def collect(): + async for event in translator.to_agui( + result, + _run_input(), + emit_messages_snapshot=False, + **{state_arg: failing_state}, + ): + collected.append(event) + + with pytest.raises(RuntimeError, match="state failed"): + asyncio.run(collect()) + + assert [ + event.type + for event in collected + if event.type in {EventType.RUN_STARTED, EventType.RUN_ERROR} + ] == [EventType.RUN_STARTED, EventType.RUN_ERROR] result.cancel.assert_called_once() @@ -494,26 +628,26 @@ async def close_on_run_finished(): result.cancel.assert_not_called() -def test_to_agui_emits_run_error_on_cancelled_error(): - # asyncio.CancelledError is BaseException, not Exception (3.8+) — a - # mid-stream timeout/dropped-connection must still surface RUN_ERROR - # instead of silently ending the stream after whatever was last yielded. +def test_to_agui_cancels_without_run_error_on_cancelled_error(): class _CancelledOutbound(_StubOutbound): def translate(self, openai_event): raise asyncio.CancelledError() translator = AGUITranslator(outbound_cls=_CancelledOutbound) + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _fake_stream("a") collected: list = [] async def collect(): - async for e in translator.to_agui(_fake_stream("a"), _run_input()): + async for e in translator.to_agui(result, _run_input()): collected.append(e) with pytest.raises(asyncio.CancelledError): asyncio.run(collect()) assert isinstance(collected[0], RunStartedEvent) - assert isinstance(collected[-1], RunErrorEvent) + assert not any(isinstance(event, RunErrorEvent) for event in collected) + result.cancel.assert_called_once() # ── AGUITranslator.to_agui (state snapshot) ────────────────────────────── From 5d1bce06a726ecc57fb7368df64a04fcaae8b9ca Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 06:44:10 +0200 Subject: [PATCH 69/94] refactor(openai-agents): simplify lifecycle finalization --- integrations/openai-agents/python/README.md | 36 ++++++-------- .../python/src/ag_ui_openai_agents/agent.py | 13 ++--- .../src/ag_ui_openai_agents/translator.py | 18 +++---- .../python/tests/test_translator.py | 49 ++++++++++++++----- 4 files changed, 64 insertions(+), 52 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index f521124f65..ab5bb80c06 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -26,7 +26,7 @@ async for event in translator.to_agui(result, run_input): ``` `to_agui(result, run_input)` also accepts `result.stream_events()` if your code already -has the SDK event iterator. The stream is always wrapped with `RUN_STARTED` +has the SDK event iterator. Ordinary runs are wrapped with `RUN_STARTED` (first) and `RUN_FINISHED`/`RUN_ERROR` (last); `thread_id` and `run_id` come from `run_input`. The event just before `RUN_FINISHED` is a `MESSAGES_SNAPSHOT` by default — see @@ -299,7 +299,7 @@ the lifecycle IDs and message history for snapshots. | `final_state` | `None` | Static, sync, or async source for a final `STATE_SNAPSHOT`. | | `emit_messages_snapshot` | `True` | Add `MESSAGES_SNAPSHOT` before the terminal event. | | `end_custom_event` | `None` | A `CustomEvent` sent after the snapshots and before `RUN_FINISHED`; its value may be static or a sync/async factory. | -| `emit_run_error` | `True` | Send `RUN_ERROR` if streaming raises, then re-raise the original exception. | +| `emit_run_error` | `True` | Send `RUN_ERROR` for an ordinary lifecycle error, then re-raise it. | | `run_error_message` | `None` | Client-safe error text; by default the event uses `str(exception)`. | `initial_state` and `final_state` can each be a value, a zero-argument @@ -322,9 +322,9 @@ end_custom_event (optional) RUN_FINISHED ``` -If the SDK stream raises, the translator emits `RUN_ERROR` when enabled and -re-raises the original exception. It does not emit final state, a messages -snapshot, `end_custom_event`, or `RUN_FINISHED` on that error path. +If lifecycle processing raises an ordinary exception, the translator emits +`RUN_ERROR` when enabled and re-raises it. It does not emit final state, a +messages snapshot, `end_custom_event`, or `RUN_FINISHED` on that error path. ### `OpenAIAgentsAgent` @@ -463,16 +463,10 @@ kept as-is. Only a `None`/empty spec becomes an empty (parameter-less) schema. ### Lifecycle Events -`to_agui` always wraps the stream: `RUN_STARTED` is the first event -yielded, and `RUN_FINISHED` is the last — or `RUN_ERROR` if the stream -raises, in which case the exception is re-raised after that event so your -own logging/observability still sees it. This covers `asyncio.CancelledError` -too (a mid-stream timeout or dropped connection), not just ordinary -exceptions — it's `BaseException`, not `Exception`, so a plain -`except Exception` would miss it and the client would just see the -stream stop with no `RUN_ERROR` and no `RUN_FINISHED`. `RUN_STARTED` and -`RUN_FINISHED` are not optional — every caller needs them, so there's no -flag to turn them off: +`to_agui` starts ordinary runs with `RUN_STARTED` and ends them with +`RUN_FINISHED`, or with `RUN_ERROR` when lifecycle processing raises an +ordinary exception. The original exception is then re-raised for application +logging. `RUN_STARTED` and `RUN_FINISHED` are not configurable: ```python async for event in translator.to_agui(result, run_input): @@ -495,9 +489,8 @@ async for event in translator.to_agui( yield encoder.encode(event) ``` -Pass `emit_run_error=False` only if you emit your own terminal error event -in an outer handler — otherwise the exception re-raises with no `RUN_ERROR` -and the client just watches the stream stop. +Pass `emit_run_error=False` only if an outer handler emits the terminal error; +otherwise an ordinary exception re-raises without `RUN_ERROR`. Open windows are closed before the error goes out: a run that dies while an assistant message is streaming still gets its `TEXT_MESSAGE_END` (and any @@ -510,10 +503,9 @@ message spinning forever. If the client goes away, the server stops iterating `to_agui` and `to_agui` cancels the SDK run for you (`RunResultStreaming.cancel()`) — the run is a separate task, and left alone it keeps calling the model and running tools for -a client that will never see the result. Any exit before `RUN_FINISHED` -triggers it: the generator being closed, the request task being cancelled -(even while a state source is resolving), or a stream error — not just a -disconnect mid-token-stream. +a client that will never see the result. Generator closure and task +cancellation emit no further events because the client cannot receive them. +Any incomplete exit still cancels the owned run. This only applies when you hand `to_agui` the `RunResultStreaming` object: diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index edfed6f31c..586f1b6d77 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -14,12 +14,8 @@ class OpenAIAgentsAgent: The wrapper is reusable across requests. Client-declared tools are added to a per-request agent clone, and output translation uses fresh per-run state. - This is the opinionated shortcut: it hides the run loop (to_openai, - Runner.run_streamed, to_agui) behind one call. If you need the run itself — - real token usage, interruptions, anything off the RunResultStreaming — use - the translator directly instead (see AGUITranslator); it hands you the - result so you can close over it. The wrapper deliberately does not grow a - parameter per such need. + This shortcut hides the run loop. Use ``AGUITranslator`` directly when + application logic needs access to ``RunResultStreaming``. Example: from agents import Agent @@ -66,8 +62,9 @@ def __init__( RUN_FINISHED. Defaults to True. end_custom_event: CustomEvent emitted before the terminal event. Its value may be static or a zero-argument sync/async factory. - To read the run result, compose the translator directly. - emit_run_error: Whether to emit RUN_ERROR when streaming fails. + Use the translator directly when its value needs the run result. + emit_run_error: Whether to emit RUN_ERROR for ordinary lifecycle + errors. Defaults to True. run_error_message: Fixed RUN_ERROR message. None sends str(exc). """ diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index fa1e099b77..efd83cb91f 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -96,8 +96,8 @@ async def to_agui( 7. end_custom_event (optional) 8. RUN_FINISHED — always last; RUN_ERROR on failure - Errors yield ``RUN_ERROR`` by default, then the original exception is - re-raised. See the README for state, snapshots, and error details. + Ordinary errors close open stream windows, yield ``RUN_ERROR`` by + default, and re-raise. Cancellation only stops an owned SDK run. Args: events: A ``RunResultStreaming`` or its ``stream_events()`` iterator. @@ -113,7 +113,8 @@ async def to_agui( end_custom_event: ``CustomEvent`` emitted before ``RUN_FINISHED``. Its value may be static or a zero-argument sync/async factory resolved just before emission. - emit_run_error: Emit ``RUN_ERROR`` before re-raising a stream error. + emit_run_error: Emit ``RUN_ERROR`` before re-raising an ordinary + error. run_error_message: Safe fixed ``RUN_ERROR`` message; defaults to ``str(exc)``. Yields: @@ -122,9 +123,8 @@ async def to_agui( Raises: TypeError: A custom event is not a CustomEvent instance. """ - # Actual failures emit RUN_ERROR; cancellation only stops an owned SDK run. lifecycle_completed = False - outbound: OpenAIToAGUITranslator | None = None + outbound = self._outbound_cls() try: # 1. RUN_STARTED — always first yield RunStartedEvent( @@ -154,14 +154,13 @@ async def to_agui( if isinstance(events, RunResultStreaming) else events ) - outbound = self._outbound_cls() try: # Streamed STEP / TEXT / TOOL / REASONING events. async for openai_event in stream_events: for event in outbound.translate(openai_event): yield event except ClientToolPending: - # Finish client-owned tool calls through the shared steps below. + # Client-owned tools complete through the shared path below. pass # 4. Close any text, tool, reasoning, or step window still open. @@ -192,9 +191,8 @@ async def to_agui( lifecycle_completed = True yield run_finished_event except Exception as exc: - if outbound is not None: - for event in outbound.finalize(): - yield event + for event in outbound.finalize(): + yield event if emit_run_error: yield RunErrorEvent( type=EventType.RUN_ERROR, diff --git a/integrations/openai-agents/python/tests/test_translator.py b/integrations/openai-agents/python/tests/test_translator.py index 39dbb7ed71..1615572ebc 100644 --- a/integrations/openai-agents/python/tests/test_translator.py +++ b/integrations/openai-agents/python/tests/test_translator.py @@ -6,8 +6,8 @@ - ``to_openai`` delegates to the inbound engine and populates ``tools``. - ``AGUITranslator.to_agui`` streams engine output live and appends the engine flush, with a fresh engine per call (reusable translator). -- ``to_agui`` always wraps the stream with RUN_STARTED / RUN_FINISHED / - RUN_ERROR — not optional, thread_id/run_id come straight off run_input. +- ``to_agui`` wraps ordinary runs with RUN_STARTED and a terminal lifecycle + event; cancellation stops an owned SDK run without emitting RUN_ERROR. - ``to_agui`` appends a MESSAGES_SNAPSHOT by default just before RUN_FINISHED; snapshot content itself is covered in ``engine/test_openai_to_agui_snapshot.py``, @@ -39,6 +39,7 @@ UserMessage, ) from ag_ui_openai_agents import AGUITranslator +from ag_ui_openai_agents.engine import ClientToolPending def _run_input( @@ -377,6 +378,37 @@ async def collect(): assert isinstance(collected[-1], RunErrorEvent) +def test_to_agui_client_tool_pending_uses_the_normal_completion_path(): + class _ClientToolOutbound(_StubOutbound): + def translate(self, openai_event): + raise ClientToolPending("confirm", "call-1", "{}") + + translator = AGUITranslator(outbound_cls=_ClientToolOutbound) + end = CustomEvent(type=EventType.CUSTOM, name="end", value={}) + + async def collect(): + return [ + event + async for event in translator.to_agui( + _fake_stream("tool"), + _run_input(), + final_state={"status": "pending"}, + end_custom_event=end, + ) + ] + + events = asyncio.run(collect()) + assert any( + isinstance(event, CustomEvent) and event.name == "finalized" + for event in events + ) + assert any(isinstance(event, StateSnapshotEvent) for event in events) + assert any(isinstance(event, MessagesSnapshotEvent) for event in events) + assert end in events + assert isinstance(events[-1], RunFinishedEvent) + assert not any(isinstance(event, RunErrorEvent) for event in events) + + def test_to_agui_resolves_custom_event_values_at_their_lifecycle_positions(): translator = AGUITranslator(outbound_cls=_StubOutbound) state = {"status": "starting"} @@ -549,9 +581,7 @@ async def collect(): def test_to_agui_cancels_the_sdk_run_when_the_consumer_stops_reading( events_to_read, closed_at ): - # A disconnected client closes this generator wherever it happens to be - # paused; without the cancel the SDK run keeps calling the model and - # running tools for nobody, whichever yield that was. + # Closing at any yield must stop the SDK run owned by this generator. translator = AGUITranslator(outbound_cls=_StubOutbound) result = MagicMock(spec=RunResultStreaming) result.stream_events.return_value = _fake_stream("a", "b") @@ -568,10 +598,7 @@ async def stop_after(event_count: int): def test_to_agui_cancels_the_sdk_run_when_a_pending_step_is_cancelled(): - # Task cancellation raises CancelledError at an await, not GeneratorExit - # at a yield — here while resolving an async initial_state source, before - # the stream section's own handler is even in play. The run must still be - # cancelled. + # Cancellation while awaiting initial state must still stop the SDK run. translator = AGUITranslator(outbound_cls=_StubOutbound) result = MagicMock(spec=RunResultStreaming) result.stream_events.return_value = _fake_stream("a") @@ -609,9 +636,7 @@ async def collect(): def test_to_agui_does_not_cancel_when_closed_right_after_run_finished(): - # A generator pauses at yield, so the consumer holds RUN_FINISHED while - # to_agui is still suspended on that line. Closing there is a normal end, - # not an interruption — the run finished, nothing is left to cancel. + # Closing while paused at the terminal yield is normal completion. translator = AGUITranslator(outbound_cls=_StubOutbound) result = MagicMock(spec=RunResultStreaming) result.stream_events.return_value = _fake_stream("a") From cf4ce0ee2107451392b2dee52c360e6999ec462a Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 07:28:12 +0200 Subject: [PATCH 70/94] refactor(openai-agents): remove old translator server example --- apps/dojo/src/files.json | 4 +- .../openai-agents/python/examples/README.md | 6 +- .../examples/agents_examples/__init__.py | 82 +---- .../human_in_the_loop_approval.py | 2 +- .../examples/tests/test_translator_server.py | 177 ---------- .../python/examples/translator_server.py | 304 ------------------ 6 files changed, 6 insertions(+), 569 deletions(-) delete mode 100644 integrations/openai-agents/python/examples/tests/test_translator_server.py delete mode 100644 integrations/openai-agents/python/examples/translator_server.py diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index c955c3ad95..f5d303b776 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4828,7 +4828,7 @@ }, { "name": "ag_ui_docs_copilot.py", - "content": "\"\"\"AG-UI documentation assistant with two focused specialists as tools.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom agents import Agent, Runner\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# ag-ui-protocol specialist: knows the core Python SDK documentation\nAG_UI_PROTOCOL_DOCS = Path(__file__).resolve().parents[5].joinpath(\n \"sdks/python/README.md\"\n).read_text(encoding=\"utf-8\")\nAG_UI_PROTOCOL_DOCS_INSTRUCTIONS = f\"\"\"You are the technical specialist for\nAG-UI Protocol's core Python SDK. The documentation below is your source of\ntruth. Help developers understand the ag_ui.core data models, RunAgentInput,\nmessages, events, event types, and ag_ui.encoder EventEncoder, including how to\ncreate and stream protocol events.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial or unrelated integration\ndetails. Be concise and practical. For code requests, provide only the\nsmallest complete, production-readable Python snippet needed for the request,\nfollowed by a short explanation. Use documented APIs only. If the\ndocumentation does not establish an answer, say so briefly instead of\nguessing.\n\n\n{AG_UI_PROTOCOL_DOCS}\n\n\"\"\"\n\nag_ui_protocol_docs_agent = Agent(\n name=\"AG-UI Protocol Python Specialist\",\n model=DEFAULT_MODEL,\n instructions=AG_UI_PROTOCOL_DOCS_INSTRUCTIONS,\n)\n\n\n# ag-ui-openai-agents specialist: knows the integration documentation\nAG_UI_OPENAI_AGENTS_DOCS = Path(__file__).resolve().parents[2].joinpath(\n \"README.md\"\n).read_text(encoding=\"utf-8\")\nAG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS = f\"\"\"You are the technical specialist for\nAG-UI OpenAI Agents integration. The documentation below is your source of\ntruth. Help developers understand and implement the integration: the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\n\n{AG_UI_OPENAI_AGENTS_DOCS}\n\n\"\"\"\n\nag_ui_openai_agents_docs_agent = Agent(\n name=\"AG-UI OpenAI Agents Specialist\",\n model=DEFAULT_MODEL,\n instructions=AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS,\n)\n\n\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call the relevant specialist\nbefore answering. Use ask_ag_ui_openai_agents_docs for the OpenAI Agents SDK\nintegration and ask_ag_ui_protocol_docs for ag_ui.core protocol types and\nEventEncoder questions.\nUse only the part of the specialist's result that answers the user's question.\nDo not invent behavior or claim details a specialist did not provide. For code\nrequests, make sure the relevant specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n ag_ui_protocol_docs_agent.as_tool(\n tool_name=\"ask_ag_ui_protocol_docs\",\n tool_description=(\n \"Provide authoritative AG-UI core Python SDK guidance about \"\n \"protocol types, RunAgentInput, events, and EventEncoder.\"\n ),\n ),\n ag_ui_openai_agents_docs_agent.as_tool(\n tool_name=\"ask_ag_ui_openai_agents_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n ),\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", + "content": "\"\"\"AG-UI documentation assistant with two focused specialists as tools.\"\"\"\n\nfrom __future__ import annotations\n\nfrom importlib.metadata import metadata\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\nfrom .docs_search import MarkdownSearchIndex\n\n\ndef _load_distribution_readme(distribution: str) -> str:\n \"\"\"Load the installed package README from its distribution metadata.\"\"\"\n document = metadata(distribution).get_payload()\n if not isinstance(document, str) or not document.strip():\n raise RuntimeError(f\"{distribution} has no README in its package metadata\")\n return document\n\n\n# ag-ui-protocol specialist: knows the core Python SDK documentation.\nAG_UI_PROTOCOL_DOCS = _load_distribution_readme(\"ag-ui-protocol\")\n_AG_UI_PROTOCOL_DOCS_INDEX = MarkdownSearchIndex(AG_UI_PROTOCOL_DOCS)\n\n\n@function_tool\ndef search_ag_ui_protocol_docs(query: str) -> str:\n \"\"\"Find relevant AG-UI Protocol Python documentation sections.\n\n Args:\n query: The documentation question or API concepts to find.\n \"\"\"\n return _AG_UI_PROTOCOL_DOCS_INDEX.search(query)\n\n\nAG_UI_PROTOCOL_DOCS_INSTRUCTIONS = \"\"\"You are the technical specialist for\nAG-UI Protocol's core Python SDK. Help developers understand ag_ui.core data\nmodels, RunAgentInput, messages, events, event types, and EventEncoder.\n\nBefore answering, always call search_ag_ui_protocol_docs with a focused query.\nTreat its excerpts as your only source of truth. If the first search is\ninsufficient, search once more with different API terms.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial or unrelated integration\ndetails. Be concise and practical. For code requests, provide only the\nsmallest complete, production-readable Python snippet needed for the request,\nfollowed by a short explanation. Use documented APIs only. If the\ndocumentation does not establish an answer, say so briefly instead of\nguessing.\n\"\"\"\n\nag_ui_protocol_docs_agent = Agent(\n name=\"AG-UI Protocol Python Specialist\",\n model=DEFAULT_MODEL,\n instructions=AG_UI_PROTOCOL_DOCS_INSTRUCTIONS,\n tools=[search_ag_ui_protocol_docs],\n)\n\n\n# ag-ui-openai-agents specialist: knows the integration documentation.\nAG_UI_OPENAI_AGENTS_DOCS = _load_distribution_readme(\"ag-ui-openai-agents\")\n_AG_UI_OPENAI_AGENTS_DOCS_INDEX = MarkdownSearchIndex(AG_UI_OPENAI_AGENTS_DOCS)\n\n\n@function_tool\ndef search_ag_ui_openai_agents_docs(query: str) -> str:\n \"\"\"Find relevant AG-UI OpenAI Agents integration documentation sections.\n\n Args:\n query: The documentation question or integration concepts to find.\n \"\"\"\n return _AG_UI_OPENAI_AGENTS_DOCS_INDEX.search(query)\n\n\nAG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS = \"\"\"You are the technical specialist for\nthe AG-UI OpenAI Agents integration. Help developers understand the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nBefore answering, always call search_ag_ui_openai_agents_docs with a focused\nquery. Treat its excerpts as your only source of truth. If the first search is\ninsufficient, search once more with different API terms.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\"\"\"\n\nag_ui_openai_agents_docs_agent = Agent(\n name=\"AG-UI OpenAI Agents Specialist\",\n model=DEFAULT_MODEL,\n instructions=AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS,\n tools=[search_ag_ui_openai_agents_docs],\n)\n\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call the relevant specialist\nbefore answering. Use ask_ag_ui_openai_agents_docs for the OpenAI Agents SDK\nintegration and ask_ag_ui_protocol_docs for ag_ui.core protocol types and\nEventEncoder questions.\nUse only the part of the specialist's result that answers the user's question.\nDo not invent behavior or claim details a specialist did not provide. For code\nrequests, make sure the relevant specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n ag_ui_protocol_docs_agent.as_tool(\n tool_name=\"ask_ag_ui_protocol_docs\",\n tool_description=(\n \"Provide authoritative AG-UI core Python SDK guidance about \"\n \"protocol types, RunAgentInput, events, and EventEncoder.\"\n ),\n ),\n ag_ui_openai_agents_docs_agent.as_tool(\n tool_name=\"ask_ag_ui_openai_agents_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n ),\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", "language": "python", "type": "file" } @@ -4914,7 +4914,7 @@ }, { "name": "human_in_the_loop_approval.py", - "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe run loop (``server.py`` / ``translator_server.py``) checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI, HTTPException\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\ndef _resolve_approval(\n pending_state: Any,\n forwarded_props: Any,\n) -> tuple[Any, bool | None]:\n \"\"\"Validate a decision for the paused run before an SSE response starts.\"\"\"\n decision = None\n if isinstance(forwarded_props, dict):\n decision = forwarded_props.get(\"approval\")\n\n if pending_state is None:\n if decision is not None:\n raise HTTPException(status_code=409, detail=\"No approval is pending\")\n return None, None\n\n if not isinstance(decision, dict):\n raise HTTPException(status_code=409, detail=\"This thread is waiting for approval\")\n\n call_id = decision.get(\"call_id\")\n approve = decision.get(\"approve\")\n if not call_id or not isinstance(approve, bool):\n raise HTTPException(status_code=409, detail=\"Invalid approval decision\")\n\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == call_id\n ),\n None,\n )\n if item is None:\n raise HTTPException(status_code=409, detail=\"Approval call_id does not match\")\n return item, approve\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n pending_state = _pending_approvals.get(body.thread_id)\n item, approve = _resolve_approval(pending_state, body.forwarded_props)\n\n async def stream():\n if item is not None:\n if approve:\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n del _pending_approvals[body.thread_id]\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_openai(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n raw_events = [event async for event in result.stream_events()]\n end_custom_event = None\n if result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", + "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe example's run loop checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom ag_ui_openai_agents.engine import ClientToolPending\nfrom .constants import DEFAULT_MODEL\n\nlogger = logging.getLogger(__name__)\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\ndef resolve_approval(\n store: dict[str, Any],\n thread_id: str,\n forwarded_props: Any,\n) -> tuple[Any, Any, bool]:\n \"\"\"Claim the paused run for this request when a matching decision arrived.\n\n Pops first: whoever gets here wins, so a double-clicked Approve can't\n resume the same run twice. Anything other than a decision that matches a\n pending interruption abandons the paused run and starts a fresh turn —\n the user moved on, and a thread should never be stuck waiting forever.\n\n ``approve`` has to be a real bool. Truthiness would read the string\n \"false\" as approval and run a refund the user declined, so a malformed\n decision is treated as no decision at all.\n\n Args:\n store: thread_id -> paused RunState.\n thread_id: The thread this request belongs to.\n forwarded_props: The request's forwarded_props.\n\n Returns:\n (pending_state, item, approve) to resume, or (None, None, False) to\n run the request fresh.\n \"\"\"\n pending_state = store.pop(thread_id, None)\n if pending_state is None:\n return None, None, False\n\n decision = None\n if isinstance(forwarded_props, dict):\n decision = forwarded_props.get(\"approval\")\n if not isinstance(decision, dict):\n return None, None, False\n\n approve = decision.get(\"approve\")\n if not isinstance(approve, bool):\n return None, None, False\n\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is None:\n return None, None, False\n return pending_state, item, approve\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n pending_state, item, approve = resolve_approval(\n _pending_approvals, body.thread_id, body.forwarded_props\n )\n\n async def stream():\n if item is not None:\n if approve:\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_openai(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n # Collect as we go rather than in one comprehension: whatever streamed\n # before a mid-drain stop still has to reach the client. A client-owned\n # tool ends the run cleanly here; a real failure is kept and re-raised\n # from replay() below, so to_agui sees it and handles it the same way\n # it would on any other route.\n raw_events = []\n stream_error = None\n try:\n async for event in result.stream_events():\n raw_events.append(event)\n except ClientToolPending:\n pass\n except Exception as exc:\n stream_error = exc\n\n end_custom_event = None\n if stream_error is None and result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n if stream_error is not None:\n raise stream_error\n\n try:\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n except Exception:\n # to_agui already sent RUN_ERROR before re-raising; log the real\n # traceback here rather than let it escape the response.\n logger.exception(\"Agent run failed\")\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", "language": "python", "type": "file" } diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 5401ee20d6..1482272864 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -1,7 +1,7 @@ # OpenAI Agents SDK examples Runnable demos for `ag_ui_openai_agents`, one mounted FastAPI app per agent. -The aggregate server shows both integration styles: +The aggregate server mounts each example application: - **`ag_ui_docs_copilot`** handles normal conversation with a small main Copilot and delegates integration questions to an @@ -10,8 +10,6 @@ The aggregate server shows both integration styles: - The remaining focused feature apps use **`OpenAIAgentsAgent`** and `add_openai_agents_fastapi_endpoint` where their run does not require custom control. -- **`translator_server.py`** remains a compact, centralized direct-translator - reference for the original focused demos. Model provider is **native OpenAI** (`OPENAI_API_KEY`). These examples exercise the direct OpenAI path deliberately, since that is the reference setup for the @@ -134,7 +132,7 @@ AG-UI concept. Unlike `human_in_the_loop`, the tool has a real server-side implementation; the SDK itself pauses the run and reports a `ToolApprovalItem` on `result.interruptions` before the body ever executes. That's only known once the stream is fully drained, so this demo is -hand-routed (see `server.py` / `translator_server.py`) instead of running +hand-routed in its own application instead of running through the shared loop: 1. First request runs normally; if `result.interruptions` is non-empty after diff --git a/integrations/openai-agents/python/examples/agents_examples/__init__.py b/integrations/openai-agents/python/examples/agents_examples/__init__.py index 045bb6f10e..483ed985e2 100644 --- a/integrations/openai-agents/python/examples/agents_examples/__init__.py +++ b/integrations/openai-agents/python/examples/agents_examples/__init__.py @@ -1,81 +1 @@ -"""Example agent factories for the AG-UI × OpenAI Agents SDK server. - -Each demo is a ``DemoConfig`` wrapping the SDK agent. ``build_registry()`` -assembles the full name → config map the aggregate servers serve, one route per key. - -Most demos only set ``agent``. custom_lifecycle_events also sets -``start_custom_event``/``end_custom_event``: optional callables -returning a ``CustomEvent``, which the shared loop forwards into -``to_agui(..., start_custom_event=..., end_custom_event=...)`` when present. -No if-branch keyed by demo name, no separate router — one generic loop, -one optional per-demo hook. - -Stateful demos (shared_state, agentic_generative_ui, -predictive_state_updates) are shelved together with ``AGUIContext`` — -see ``.dev/shelved/``. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Callable - -from agents import Agent - -from ag_ui.core import CustomEvent -from .agentic_chat import create_agentic_chat_agent -from .backend_tool_rendering import create_backend_tool_agent -from .custom_lifecycle_events import ( - build_input_usage_event, - build_output_usage_event, - create_custom_lifecycle_events_agent, -) -from .dynamic_system_prompt import create_dynamic_system_prompt_agent -from .human_in_the_loop import create_human_in_the_loop_agent -from .human_in_the_loop_approval import create_human_in_the_loop_approval_agent -from .subagents import create_subagents_agent -from .tool_based_generative_ui import create_tool_based_generative_ui_agent - - -@dataclass(frozen=True) -class DemoConfig: - """One demo route: the agent to run for it, plus optional lifecycle hooks.""" - - agent: Agent - start_custom_event: CustomEvent | Callable[[], CustomEvent] | None = None - end_custom_event: CustomEvent | Callable[[], CustomEvent] | None = None - - -def build_registry() -> dict[str, DemoConfig]: - """Assemble the demo registry served by ``server.py`` (one route per key).""" - return { - "agentic_chat": DemoConfig(agent=create_agentic_chat_agent()), - "backend_tool_rendering": DemoConfig(agent=create_backend_tool_agent()), - "human_in_the_loop": DemoConfig(agent=create_human_in_the_loop_agent()), - # Same idea as human_in_the_loop (pause for a person before an action - # happens) but a different mechanism — see - # human_in_the_loop_approval.py's docstring for the frontend-tool vs - # SDK-native-approval distinction. - "human_in_the_loop_approval": DemoConfig(agent=create_human_in_the_loop_approval_agent()), - "tool_based_generative_ui": DemoConfig(agent=create_tool_based_generative_ui_agent()), - "subagents": DemoConfig(agent=create_subagents_agent()), - "custom_lifecycle_events": DemoConfig( - agent=create_custom_lifecycle_events_agent(), - start_custom_event=build_input_usage_event, - end_custom_event=build_output_usage_event, - ), - "dynamic_system_prompt": DemoConfig(agent=create_dynamic_system_prompt_agent()), - } - - -__all__ = [ - "DemoConfig", - "build_registry", - "create_agentic_chat_agent", - "create_backend_tool_agent", - "create_custom_lifecycle_events_agent", - "create_human_in_the_loop_agent", - "create_human_in_the_loop_approval_agent", - "create_subagents_agent", - "create_tool_based_generative_ui_agent", -] +"""OpenAI Agents SDK example applications.""" diff --git a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py index da52703553..8591ef82e2 100644 --- a/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py +++ b/integrations/openai-agents/python/examples/agents_examples/human_in_the_loop_approval.py @@ -11,7 +11,7 @@ ``result.interruptions``. That only becomes known once the stream is fully drained — there is no mid-stream event for it — so it can't go through the normal per-item translator dispatch the way ``MCPApprovalRequestItem`` does. -The run loop (``server.py`` / ``translator_server.py``) checks +The example's run loop checks ``result.interruptions`` right after ``to_agui()`` finishes, and if any are pending: diff --git a/integrations/openai-agents/python/examples/tests/test_translator_server.py b/integrations/openai-agents/python/examples/tests/test_translator_server.py deleted file mode 100644 index bc0c2255e7..0000000000 --- a/integrations/openai-agents/python/examples/tests/test_translator_server.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Regression tests for the direct-translator example server.""" - -from __future__ import annotations - -import asyncio -import sys -from pathlib import Path -from types import SimpleNamespace -from typing import Any - -import pytest - -EXAMPLES = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(EXAMPLES)) - -import translator_server # noqa: E402 -from agents_examples import human_in_the_loop_approval # noqa: E402 -from ag_ui.core import Context, RunAgentInput, UserMessage # noqa: E402 - - -class _PendingState: - def __init__(self, call_id: str = "call_1") -> None: - self.item = SimpleNamespace(raw_item=SimpleNamespace(call_id=call_id)) - - def get_interruptions(self) -> list[Any]: - return [self.item] - - -def _run_input(forwarded_props: Any = None) -> RunAgentInput: - return RunAgentInput( - thread_id="thread_1", - run_id="run_1", - messages=[UserMessage(id="message_1", role="user", content="hello")], - tools=[], - state={}, - context=[], - forwarded_props=forwarded_props, - ) - - -@pytest.mark.parametrize( - "forwarded_props", - [ - None, - {"approval": {"call_id": "wrong_call", "approve": True}}, - {"approval": {"call_id": "call_1"}}, - ], -) -@pytest.mark.parametrize( - ("endpoint", "store"), - [ - (human_in_the_loop_approval.run, human_in_the_loop_approval._pending_approvals), - ( - lambda body: translator_server.run("human_in_the_loop_approval", body), - translator_server._PENDING_APPROVALS, - ), - ], -) -def test_a_request_without_a_valid_decision_starts_a_fresh_run( - forwarded_props: Any, - endpoint: Any, - store: dict[str, object], -) -> None: - # Both servers answer normally instead of refusing the request: a user who - # types something else instead of deciding abandons the paused run, and a - # thread that lost its approval card can still be talked to. - store.clear() - store["thread_1"] = _PendingState() - - try: - response = asyncio.run(endpoint(_run_input(forwarded_props))) - - assert response.status_code == 200 - assert store == {} - finally: - store.clear() - - -def test_approval_stream_error_keeps_what_already_streamed( - monkeypatch: pytest.MonkeyPatch, -) -> None: - # The hand-drain is the only place an SDK failure can land outside - # to_agui's wrapper. Re-raising it from the replay puts it back inside, so - # the client still gets the events that made it out before the failure — - # and one RUN_ERROR, built by the translator like on every other route. - boom = RuntimeError("provider exploded") - - async def stream_events(): - yield "event_1" - raise boom - - result = SimpleNamespace( - stream_events=stream_events, - interruptions=[], - to_state=lambda: None, - ) - monkeypatch.setattr( - translator_server.Runner, "run_streamed", lambda *a, **k: result - ) - - seen: dict[str, Any] = {} - - async def to_agui(events: Any, body: Any, **kwargs: Any): - seen["replayed"] = [event async for event in _drain(events, seen)] - yield SimpleNamespace(type="RUN_ERROR") - - async def _drain(events: Any, sink: dict[str, Any]): - try: - async for event in events: - yield event - except RuntimeError as exc: - sink["raised"] = exc - - monkeypatch.setattr( - translator_server, - "translator", - SimpleNamespace( - to_openai=lambda run_input: SimpleNamespace( - messages=[], tools=[], context=[] - ), - to_agui=to_agui, - ), - ) - - async def collect() -> list[Any]: - return [ - chunk - async for chunk in translator_server._stream_approval( - SimpleNamespace(agent=SimpleNamespace(tools=[])), - _run_input(), - None, - None, - False, - ) - ] - - asyncio.run(collect()) - - assert seen["replayed"] == ["event_1"], "partial output must survive the failure" - assert seen["raised"] is boom, "the error must reach to_agui, not be swallowed" - - -def test_direct_server_forwards_context_to_the_sdk(monkeypatch: pytest.MonkeyPatch) -> None: - context = [Context(description="Response language", value="German")] - body = _run_input() - body.context = context - translated = SimpleNamespace(messages=[{"role": "user"}], tools=[], context=context) - captured: dict[str, Any] = {} - result = object() - - async def to_agui(*args: Any, **kwargs: Any): - if False: - yield None - - fake_translator = SimpleNamespace( - to_openai=lambda run_input: translated, - to_agui=to_agui, - ) - - def run_streamed(agent: Any, *, input: Any, context: Any) -> object: - captured.update(agent=agent, input=input, context=context) - return result - - monkeypatch.setattr(translator_server, "translator", fake_translator) - monkeypatch.setattr(translator_server.Runner, "run_streamed", run_streamed) - - demo = SimpleNamespace( - agent=object(), - start_custom_event=None, - end_custom_event=None, - ) - - async def collect() -> list[Any]: - return [chunk async for chunk in translator_server._stream(demo, body)] - - assert asyncio.run(collect()) == [] - assert captured["context"] is context diff --git a/integrations/openai-agents/python/examples/translator_server.py b/integrations/openai-agents/python/examples/translator_server.py deleted file mode 100644 index e8e75ebf37..0000000000 --- a/integrations/openai-agents/python/examples/translator_server.py +++ /dev/null @@ -1,304 +0,0 @@ -""" -Multi-agent example server — the translator by hand. Recommended. - -Wires the translator directly — to_openai, Runner.run_streamed, to_agui — so the -full run loop is visible in one place. AGUITranslator is just an events -translator: this file keeps full control of the agent and the server. Same -demos and output as server.py, which builds the same thing with the -OpenAIAgentsAgent + add_openai_agents_fastapi_endpoint serve layer — an -opinionated shortcut that trades that control for less code. - -One FastAPI route per demo, all sharing the same run loop: - - POST /agentic_chat ← plain conversation - POST /backend_tool_rendering ← server-executed @function_tool - POST /human_in_the_loop ← frontend-owned tool, StopAtTools - POST /tool_based_generative_ui ← frontend tool renders the content - POST /orchestrator ← multi-agent via agents-as-tools - POST /custom_lifecycle_events ← manual CUSTOM event right after RUN_STARTED - and right before RUN_FINISHED, via - DemoConfig.start_custom_event / - end_custom_event - POST /human_in_the_loop_approval ← backend tool gated by needs_approval, - resumed from result.interruptions - (routed by hand — see below) - - GET /health ← liveness check - -Stateful demos (shared_state, agentic_generative_ui, -predictive_state_updates) are shelved with ``AGUIContext`` — see -``.dev/shelved/``. - -Run: - OPENAI_API_KEY=sk-... uv run python translator_server.py - - # Auto-restart on code changes (this file, agents_examples/, or the - # package's own src/ — an editable install, so plain `uvicorn.run(app)` - # never notices those edits on its own): - RELOAD=1 OPENAI_API_KEY=sk-... uv run python translator_server.py - -Test: - curl -N -X POST http://localhost:8024/agentic_chat \\ - -H 'Content-Type: application/json' \\ - -d '{ - "thread_id": "t1", - "run_id": "r1", - "messages": [{"id":"m1","role":"user","content":"Say hi in one sentence."}], - "tools": [], - "state": {}, - "context": [], - "forwarded_props": null - }' - -Expected event order for that request: - RUN_STARTED → STATE_SNAPSHOT → TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT (×N) - → TEXT_MESSAGE_END → MESSAGES_SNAPSHOT → RUN_FINISHED -""" - -from __future__ import annotations - -import logging -import os -from pathlib import Path -from typing import Any - -import uvicorn -from agents import Runner -from fastapi import FastAPI, HTTPException -from fastapi.responses import StreamingResponse - -from ag_ui.core import CustomEvent, EventType, RunAgentInput -from ag_ui.encoder import EventEncoder -from ag_ui_openai_agents import AGUITranslator -from ag_ui_openai_agents.engine import ClientToolPending -from agents_examples import DemoConfig, build_registry -from agents_examples.human_in_the_loop_approval import resolve_approval - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# --------------------------------------------------------------------------- -# Demo registry — one entry per demo, keyed by URL path -# --------------------------------------------------------------------------- - -DEMOS: dict[str, DemoConfig] = build_registry() - -# The translator is stateless/reusable — one instance serves every request; each -# to_agui call creates the fresh per-run engine it needs internally. -translator = AGUITranslator() - -# AG-UI's own SSE encoder: one `data: \n\n` frame per event. -encoder = EventEncoder() - -# --------------------------------------------------------------------------- -# FastAPI app -# --------------------------------------------------------------------------- - -app = FastAPI(title="AG-UI × OpenAI Agents SDK examples") - - -@app.get("/health") -async def health() -> dict: - return {"status": "ok", "agents": list(DEMOS)} - - -# thread_id -> paused RunState, waiting for an approve/reject decision on the -# next request. In-memory only — fine for a demo process, not a restart or a -# second server instance; a real app would use a session store instead. -_PENDING_APPROVALS: dict[str, object] = {} - - -@app.post("/{agent_name}") -async def run(agent_name: str, body: RunAgentInput) -> StreamingResponse: - """Accept a RunAgentInput, run the named demo agent, stream AG-UI events back via SSE.""" - demo = DEMOS.get(agent_name) - if demo is None: - raise HTTPException(status_code=404, detail=f"Unknown agent: {agent_name!r}") - logger.info( - "Starting agent run: agent=%s thread_id=%s run_id=%s", - agent_name, - body.thread_id, - body.run_id, - ) - if agent_name == "human_in_the_loop_approval": - pending_state, item, approve = resolve_approval( - _PENDING_APPROVALS, body.thread_id, body.forwarded_props - ) - return StreamingResponse( - _stream_approval(demo, body, pending_state, item, approve), - media_type=encoder.get_content_type(), - ) - return StreamingResponse(_stream(demo, body), media_type=encoder.get_content_type()) - - -async def _stream_approval( - demo: DemoConfig, - body: RunAgentInput, - pending_state: Any, - item: Any, - approve: bool, -): - """Same shape as _stream, plus resuming from result.interruptions. - - Kept separate from _stream rather than adding an if-branch there: this is - the only demo that needs to inspect result.interruptions after the - stream and possibly resume from a stored RunState instead of - translated.messages, and that logic doesn't apply to any other demo. - """ - agent = demo.agent - if item is not None: - if approve: - pending_state.approve(item) - else: - pending_state.reject(item) - result = Runner.run_streamed(agent, pending_state) - else: - translated = translator.to_openai(body) - if translated.tools: - agent = agent.clone(tools=[*agent.tools, *translated.tools]) - result = Runner.run_streamed( - agent, input=translated.messages, context=translated.context - ) - - # result.interruptions is only known once the SDK's own stream is fully - # drained — there's no mid-stream event for it. to_agui() always puts - # RUN_FINISHED last, and the client drops anything that arrives after - # RUN_FINISHED, so the approval CustomEvent has to go out as - # end_custom_event (right before RUN_FINISHED) — which means draining - # the raw SDK stream ourselves first instead of handing `result` - # straight to to_agui: end_custom_event has to already exist by the - # time we call it, and interruptions aren't known until the drain - # finishes. - # Collect as we go rather than in one comprehension: whatever streamed - # before a mid-drain stop still has to reach the client. A client-owned tool - # ends the run cleanly here; a real failure is kept and re-raised from - # _replay() below, so to_agui sees it and handles it the same way it would - # on any other route. - raw_events = [] - stream_error = None - try: - async for event in result.stream_events(): - raw_events.append(event) - except ClientToolPending: - pass - except Exception as exc: - stream_error = exc - - end_custom_event = None - if stream_error is None and result.interruptions: - _PENDING_APPROVALS[body.thread_id] = result.to_state() - end_custom_event = CustomEvent( - type=EventType.CUSTOM, - name="approval_request", - value=[ - { - "call_id": getattr(item.raw_item, "call_id", None), - "tool_name": item.tool_name, - "arguments": getattr(item.raw_item, "arguments", None), - } - for item in result.interruptions - ], - ) - - async def _replay(): - for event in raw_events: - yield event - if stream_error is not None: - raise stream_error - - try: - async for ag_event in translator.to_agui( - _replay(), body, end_custom_event=end_custom_event - ): - yield encoder.encode(ag_event) - except Exception: - logger.exception("Agent run failed") - - -# --------------------------------------------------------------------------- -# Core streaming logic — shared by every demo in the registry -# --------------------------------------------------------------------------- - - -async def _stream(demo: DemoConfig, body: RunAgentInput): - """ - Translate the AG-UI input → run the SDK agent → translate events back. - - Each yielded chunk is one SSE line: ``data: \\n\\n`` - """ - # 1 — Translate AG-UI input into SDK-ready shapes. - translated = translator.to_openai(body) - - # Frontend (client-owned) tools declared on this request — e.g. the - # human_in_the_loop demo's `generate_task_steps`. Merged per-request - # rather than baked into the Agent, since they come from the wire. - agent = demo.agent - if translated.tools: - agent = agent.clone(tools=[*agent.tools, *translated.tools]) - - # Demo-specific lifecycle hooks (only custom_lifecycle_events sets these) — - # start_custom_event/end_custom_event only accept CustomEvent instances - # (anything else raises TypeError), so building them here, not inline, - # keeps this loop demo-agnostic: it just forwards whatever the registry - # gave it. - to_agui_kwargs = {} - if demo.start_custom_event: - to_agui_kwargs["start_custom_event"] = ( - demo.start_custom_event() - if callable(demo.start_custom_event) - else demo.start_custom_event - ) - if demo.end_custom_event: - to_agui_kwargs["end_custom_event"] = ( - demo.end_custom_event() - if callable(demo.end_custom_event) - else demo.end_custom_event - ) - - # 2 — Run the agent; to_agui() wraps the stream with the lifecycle events - # (RUN_STARTED first, RUN_FINISHED / RUN_ERROR last), echoes the state - # snapshot, handles window bookkeeping + the final flush, and appends a - # trailing MESSAGES_SNAPSHOT. Nothing to hand-emit here. - try: - # context= carries the AG-UI context items through to run-context - # readers (dynamic_system_prompt resolves its language from them). - result = Runner.run_streamed( - agent, input=translated.messages, context=translated.context - ) - async for ag_event in translator.to_agui(result, body, **to_agui_kwargs): - yield encoder.encode(ag_event) - except Exception: - # to_agui already emitted RUN_ERROR before re-raising; just log here so - # the real traceback lands in the server logs. - logger.exception("Agent run failed") - - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - -def main() -> int: - if not os.getenv("OPENAI_API_KEY"): - print("Error: OPENAI_API_KEY required") - return 1 - # 8024 is the port the AG-UI Dojo expects for this integration - # (apps/dojo/src/env.ts — OPENAI_AGENTS_PYTHON_URL). - port = int(os.getenv("PORT", "8024")) - host = os.getenv("HOST", "0.0.0.0") - print(f"Starting server on port {port} — agents: {list(DEMOS)}") - if os.getenv("RELOAD"): - uvicorn.run( - "translator_server:app", - host=host, - port=port, - reload=True, - reload_dirs=[str(Path(__file__).parent), str(Path(__file__).parent.parent / "src")], - log_level="info", - ) - else: - uvicorn.run(app, host=host, port=port, log_level="info") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From aa972a3cd7cdb7cc643e758fe703932464f97c79 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 09:24:53 +0200 Subject: [PATCH 71/94] fix(openai-agents): remove LLMock configuration from OpenAI Agents launcher in dojo --- apps/dojo/scripts/run-dojo-everything.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/dojo/scripts/run-dojo-everything.js b/apps/dojo/scripts/run-dojo-everything.js index 951cd1ab69..b76cdb09f2 100755 --- a/apps/dojo/scripts/run-dojo-everything.js +++ b/apps/dojo/scripts/run-dojo-everything.js @@ -224,13 +224,7 @@ const ALL_SERVICES = { command: "uv run dev", name: "OpenAI Agents SDK (Python)", cwd: path.join(integrationsRoot, "openai-agents/python/examples"), - env: { - PORT: 8024, - OPENAI_API_KEY: process.env.OPENAI_API_KEY || "test-key", - ...(!process.env.OPENAI_API_KEY && { - OPENAI_BASE_URL: "http://localhost:5555/v1", - }), - }, + env: { PORT: 8024 }, }, ], "claude-agent-sdk-python": [ From 2821d4f065de7e4a1a31ee567a2a6f4b1cc02074 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 11:13:02 +0200 Subject: [PATCH 72/94] docs(openai-agents): update README for message and event mapping details --- integrations/openai-agents/python/README.md | 109 +++++++++----------- 1 file changed, 47 insertions(+), 62 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index ab5bb80c06..4fe0e6cbc2 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -586,53 +586,56 @@ The translator translates. Your run loop still owns orchestration: This keeps the integration framework-neutral. FastAPI, Starlette, Django, aiohttp, raw ASGI, WebSockets, or tests can all use the same translator calls. -### Event Mapping +### Message, Event, and ID Mapping -Both directions, source of truth is `engine/agui_to_openai.py` and -`engine/openai_to_agui.py`. +Source of truth: `engine/agui_to_openai.py` (inbound) and +`engine/openai_to_agui.py` (outbound). -**Inbound** — AG-UI `Message` → OpenAI Agents SDK input item (`AGUIToOpenAITranslator`): +#### Inbound: AG-UI → OpenAI SDK (`to_openai`) -| AG-UI type | OpenAI Agents SDK input shape | -|---|---| -| `UserMessage` | `{"type": "message", "role": "user", "content": [...]}` (multimodal-aware) | -| `SystemMessage` | `{"type": "message", "role": "system", ...}` | -| `DeveloperMessage` | `{"type": "message", "role": "developer", ...}` | -| `AssistantMessage` | optional text `message` item + one `function_call` item per `tool_calls` entry | -| `ToolMessage` | `{"type": "function_call_output", "call_id": ..., "output": ...}` | -| `ReasoningMessage` | `{"type": "reasoning", ...}` if `encrypted_value` is set, else dropped (plaintext reasoning isn't replayable) | -| `ActivityMessage` | dropped (neither Responses nor Chat Completions has an equivalent model-input item; subclasses may override this mapping) | -| `Tool` (client-declared) | SDK `FunctionTool` proxy that raises `ClientToolPending` when invoked | -| Content parts: `TextInputContent`, `ImageInputContent`, `AudioInputContent`, `DocumentInputContent`, `BinaryInputContent` | `input_text` / `input_image` / `input_audio` / `input_file` parts | -| `VideoInputContent` | dropped (neither Responses nor Chat Completions accepts video input) | - -**Outbound** — SDK stream event → AG-UI `BaseEvent` (`OpenAIToAGUITranslator`): - -| SDK event / item | AG-UI event(s) | -|---|---| -| `response.output_item.added` (message) | `TEXT_MESSAGE_START` | -| `response.output_text.delta` / `response.refusal.delta` | `TEXT_MESSAGE_CONTENT` (lazy-opens the window if needed) | -| `response.output_item.done` (message) / `response.output_text.done` | `TEXT_MESSAGE_END` | -| `response.output_item.added` (function_call / hosted tool call) | `TOOL_CALL_START` | -| `response.function_call_arguments.delta` | `TOOL_CALL_ARGS` | -| `response.output_item.done` (function_call / hosted tool call) | `TOOL_CALL_END` | -| `ToolCallOutputItem` | `TOOL_CALL_RESULT` | -| `HandoffCallItem` | `TOOL_CALL_START` / `TOOL_CALL_ARGS` / `TOOL_CALL_END` (a handoff is a function call) | -| `HandoffOutputItem` | `TOOL_CALL_RESULT` | -| `response.reasoning_summary_text.delta` / `response.reasoning_text.delta` | `REASONING_START` (once) + `REASONING_MESSAGE_START` + `REASONING_MESSAGE_CONTENT` | -| `response.reasoning_summary_part.done` / `response.reasoning_text.done` | `REASONING_MESSAGE_END` | -| `ReasoningItem` (finished, with `encrypted_content`) | `REASONING_ENCRYPTED_VALUE` | -| stream end / any open reasoning when text or a tool call opens | `REASONING_END` | -| `AgentUpdatedStreamEvent` | `STEP_FINISHED` (previous agent, if any) + `STEP_STARTED` (new agent) | -| `MCPApprovalRequestItem` | `CUSTOM` (name=`"mcp_approval_request"`) | -| `MCPListToolsItem`, `MCPApprovalResponseItem` | dropped (server-side bookkeeping / echo) | -| stream start / end (always, via `to_agui`) | `RUN_STARTED` / `RUN_FINISHED` (or `RUN_ERROR`) | -| end of stream, `run_input` given (default) | `MESSAGES_SNAPSHOT` | -| `initial_state` / `final_state`, if provided | `STATE_SNAPSHOT` | - -Unknown SDK event or item types translate to `[]` with a debug log — -graceful degradation, never a raise. See `tests/engine/test_openai_to_agui.py` for -the streaming behavior pinned event-by-event. +| AG-UI message | OpenAI SDK input item | AG-UI ID in → OpenAI ID out | +|---|---|---| +| `UserMessage` | `message` (role `user`) | `message.id` dropped, not sent. All-unsupported content parts (e.g. video-only) drop the whole message. | +| `SystemMessage` | `message` (role `system`) | `message.id` dropped, not sent. | +| `DeveloperMessage` | `message` (role `developer`) | `message.id` dropped, not sent. | +| `AssistantMessage` | `{"role": "assistant", ...}` (if text) + one `function_call` per tool call | `message.id` dropped. **`ToolCall.id` → `function_call.call_id`** (preserved 1:1). Empty text emits no item. | +| `ToolMessage` | `function_call_output` | `message.id` dropped. **`tool_call_id` → `function_call_output.call_id`** (preserved 1:1). | +| `ReasoningMessage` | `reasoning` item | **`message.id` → `reasoning.id`** (preserved 1:1), only when `encrypted_value` is set — plaintext-only reasoning is dropped, no item emitted. | +| `ActivityMessage` | *(dropped)* | No SDK equivalent; dropped with a debug log, no ID involved. | +| `RunAgentInput.tools` | `FunctionTool` proxy | No ID — tool `name` + JSON Schema pass through. | + +#### Outbound: OpenAI SDK → AG-UI (`to_agui`) + +| OpenAI SDK item / event | AG-UI events | OpenAI ID in → AG-UI ID out | +|---|---|---| +| `message` item + `output_text`/`refusal` deltas | `TEXT_MESSAGE_START` / `CONTENT` / `END` | **item `id` → `message_id`** if real, else generate `msg_`. Same ID reused in `MESSAGES_SNAPSHOT`. | +| `function_call`, hosted-tool call, or handoff call | `TOOL_CALL_START` / `ARGS` / `END` | **`call_id` → `tool_call_id`** first choice; falls back to item `id`, then generated `call_`. Same ID reused in the snapshot. | +| `function_call_output` or handoff output | `TOOL_CALL_RESULT` | **`call_id` → `tool_call_id`** (passthrough). `message_id` is *derived*, not from the wire: `-result`. Skipped entirely if `call_id` is missing. | +| `reasoning` item + summary/reasoning-text deltas | `REASONING_START`, `REASONING_MESSAGE_START`/`CONTENT`/`END` per part, `REASONING_END` | **item `id` → phase `message_id`** if real, else generate `rs_`. First part reuses the phase ID; later parts get `-1`, `-2`, … (derived). Not included in `MESSAGES_SNAPSHOT`. | +| `reasoning.encrypted_content` | `REASONING_ENCRYPTED_VALUE` (subtype `message`) | **phase `message_id` → `entity_id`** (reused, not new). Emitted at most once per reasoning item. | +| `AgentUpdatedStreamEvent` (first agent, each handoff target) | `STEP_FINISHED` (previous) then `STEP_STARTED` | No ID at all — `step_name` is the agent's `name`, or `"agent"` if unnamed. | +| `MCPApprovalRequestItem` | `CUSTOM` named `mcp_approval_request` | No AG-UI ID assigned; `value` carries the raw request as-is. | +| `MCPListToolsItem`, `MCPApprovalResponseItem` | *(dropped)* | Server-side bookkeeping; dropped with a debug log, no ID involved. | +| Run input / stream completion / error | `RUN_STARTED`, then `RUN_FINISHED`/`RUN_ERROR`; optional `STATE_SNAPSHOT`; `MESSAGES_SNAPSHOT` by default | **`RunAgentInput.thread_id`/`run_id` → same fields on the lifecycle events** (passthrough, not generated). | + +#### ID rules that apply everywhere + +- **Real wire ID always wins.** Generated only when the SDK sends none, or + sends its `FAKE_RESPONSES_ID` placeholder (some non-Responses backends + stamp every item with it). +- **Generated IDs mimic wire prefixes** (`msg_`, `call_`, `rs_`) so a + client can't tell generated from real. +- **A hyphen marks an ID this package derived** (`-result`, + `-1`) — wire IDs never contain one. +- **Every streamed ID is reused verbatim in `MESSAGES_SNAPSHOT`** — one + resolution per item, so the streamed event and the snapshot entry can never + disagree. Reasoning is the one item type excluded from the snapshot. +- **Internal-only correlation key:** raw response events key their open + windows by the real item ID, or by `__idx_` when the ID is + missing/placeholder. That key is bookkeeping — it is never put on an AG-UI + event. +- Unknown SDK message/event types are dropped with a debug log instead of + failing the run. ### Guardrails @@ -753,24 +756,6 @@ run_agent = agent.clone(instructions=instructions) result = Runner.run_streamed(run_agent, input=translated_input.messages) ``` -## Capabilities - -The streaming translator supports the OpenAI Agents SDK stream shapes AG-UI -clients care about: - -| Capability | AG-UI output | -|---|---| -| Assistant text | `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END` | -| Refusals | Text message events | -| Tool calls | `TOOL_CALL_START`, streamed `TOOL_CALL_ARGS`, `TOOL_CALL_END`, `TOOL_CALL_RESULT` | -| Reasoning text | `REASONING_START`, reasoning message events, `REASONING_END` | -| Encrypted reasoning replay data | `REASONING_ENCRYPTED_VALUE` | -| Hosted tools such as web search, file search, code interpreter | Tool call events | -| Handoffs and agents-as-tools | Tool call events plus step events | -| MCP approval requests | `CUSTOM` events | - -Unknown SDK event types are skipped with a debug log instead of crashing the run. - ## Advanced: the engine layer The public translator delegates to two independent, symmetric engine translators in From 1a388ffa7fc0f297cb647be571e946b99b43df4d Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 11:15:09 +0200 Subject: [PATCH 73/94] fix(openai-agents): correct approval and subagent demo state --- .../feature/(v2)/human_in_the_loop_approval/page.tsx | 2 ++ .../[integrationId]/feature/(v2)/subagents/page.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx index b19358250b..8eacf2296c 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx @@ -69,6 +69,8 @@ const Chat = () => { arguments: first.arguments, }); }, + onRunFinishedEvent: () => setResolvedCallId(null), + onRunErrorEvent: () => setResolvedCallId(null), }); return () => subscription.unsubscribe(); }, [agent]); diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx index 4ea57240b1..2b4329e04b 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx @@ -84,13 +84,13 @@ const SubagentsView = () => { useRenderTool({ name: "research_topic", agentId: "subagents", - parameters: z.object({ topic: z.string().optional() }), + parameters: z.object({ input: z.string().optional() }), render: ({ toolCallId, status, args, result }: any) => ( @@ -100,13 +100,13 @@ const SubagentsView = () => { useRenderTool({ name: "write_prose", agentId: "subagents", - parameters: z.object({ facts: z.string().optional() }), + parameters: z.object({ input: z.string().optional() }), render: ({ toolCallId, status, args, result }: any) => ( @@ -116,13 +116,13 @@ const SubagentsView = () => { useRenderTool({ name: "critique_draft", agentId: "subagents", - parameters: z.object({ draft: z.string().optional() }), + parameters: z.object({ input: z.string().optional() }), render: ({ toolCallId, status, args, result }: any) => ( From 17f44bbed210a7e991d9c8c7edf29827be0e8b88 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 12:09:52 +0200 Subject: [PATCH 74/94] fix(openai-agents): prevent client tool shadowing --- integrations/openai-agents/python/README.md | 2 ++ .../python/src/ag_ui_openai_agents/agent.py | 29 +++++++++++++++---- .../openai-agents/python/tests/test_agent.py | 18 +++++++++++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 4fe0e6cbc2..8a9fa59df7 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -163,6 +163,8 @@ add_openai_agents_fastapi_endpoint(app, agent, "/") - holds no per-request state — one instance serves every request; the SDK `Agent` is a config template, and client-declared tools are merged onto a per-request `clone()`, so concurrent requests never see each other's tools; +- keeps the server tool when a client-declared tool uses the same name; the + conflicting client tool is ignored with a warning; - takes `run_config=...` to set run-wide model settings; - exposes `run_streamed(RunAgentInput) -> AsyncIterator[BaseEvent]` if you want to serve it on a transport other than FastAPI. diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index 586f1b6d77..6da0dd6407 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -1,5 +1,6 @@ """Agent wrapper — an OpenAI Agents SDK Agent that speaks AG-UI.""" +import logging from typing import Any, AsyncIterator from agents import Agent, RunConfig, Runner @@ -7,12 +8,14 @@ from ag_ui.core import BaseEvent, CustomEvent, RunAgentInput from .translator import AGUITranslator +logger = logging.getLogger(__name__) + class OpenAIAgentsAgent: """Wrap an OpenAI Agents SDK Agent so it speaks the AG-UI protocol. - The wrapper is reusable across requests. Client-declared tools are added to - a per-request agent clone, and output translation uses fresh per-run state. + The wrapper is reusable across requests. Non-conflicting client tools are + added to a per-request agent clone, and output uses fresh per-run state. This shortcut hides the run loop. Use ``AGUITranslator`` directly when application logic needs access to ``RunResultStreaming``. @@ -97,9 +100,25 @@ async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: run_agent = self.agent if translated.tools: - run_agent = self.agent.clone( - tools=[*self.agent.tools, *translated.tools] - ) + server_tool_names = { + name + for tool in self.agent.tools + if (name := getattr(tool, "name", None)) is not None + } + client_tools = [] + for tool in translated.tools: + if tool.name in server_tool_names: + logger.warning( + "Ignoring client tool %r because a server tool already uses this name", + tool.name, + ) + continue + client_tools.append(tool) + + if client_tools: + run_agent = self.agent.clone( + tools=[*self.agent.tools, *client_tools] + ) result = Runner.run_streamed( run_agent, diff --git a/integrations/openai-agents/python/tests/test_agent.py b/integrations/openai-agents/python/tests/test_agent.py index 5b9e17b2cd..ea83a2e360 100644 --- a/integrations/openai-agents/python/tests/test_agent.py +++ b/integrations/openai-agents/python/tests/test_agent.py @@ -19,7 +19,7 @@ from unittest.mock import MagicMock import pytest -from agents import Agent, RunConfig +from agents import Agent, RunConfig, function_tool from agents.result import RunResultStreaming import ag_ui_openai_agents.agent as agent_module @@ -109,6 +109,22 @@ def test_run_merges_client_tools_onto_clone(patched_runner): assert sdk_agent.tools == [], "the static agent must stay untouched" +def test_run_keeps_server_tool_when_client_name_conflicts(patched_runner, caplog): + @function_tool + def confirm() -> str: + """Confirm on the server.""" + return "confirmed" + + sdk_agent = Agent(name="assistant", instructions="hi", tools=[confirm]) + wrapper = OpenAIAgentsAgent(sdk_agent) + + _collect(wrapper, _run_input(with_tool=True)) + + assert patched_runner[0]["agent"] is sdk_agent + assert [tool.name for tool in sdk_agent.tools] == ["confirm"] + assert "Ignoring client tool 'confirm'" in caplog.text + + def test_run_config_passes_through(patched_runner): run_config = RunConfig() wrapper = OpenAIAgentsAgent( From 2e7581777f788fbb4dace903c1670e4c21d27278 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 12:25:35 +0200 Subject: [PATCH 75/94] fix(openai-agents): add required image detail --- .../engine/agui_to_openai.py | 21 ++-- .../src/ag_ui_openai_agents/engine/types.py | 3 +- .../engine/test_agui_to_openai_multimodal.py | 114 ++++++++++++++++-- 3 files changed, 118 insertions(+), 20 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index d80b27d7dc..71f67a1d03 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -462,20 +462,20 @@ def translate_image_content( """Translate an ImageInputContent part. URL sources pass through unchanged. Data sources become base64 - data URLs so the Responses-API image_url field always receives - a single string. + data URLs. OpenAI requires a detail level, so AG-UI images use + ``auto``. Args: part: The AG-UI image content part. Returns: - {"type": "input_image", "image_url": ...}, or None if the - source has no usable value. + {"type": "input_image", "image_url": ..., "detail": "auto"}, + or None if the source has no usable value. """ url = self._data_source_to_url(part.source) if url is None: return None - return {"type": "input_image", "image_url": url} + return self._image_input(url) def translate_audio_content( self, @@ -683,7 +683,7 @@ def _dispatch_dict_content_part( return {"type": "input_text", "text": text} if part_type == "image": url = self._data_source_to_url(read_attr(part, "source")) - return {"type": "input_image", "image_url": url} if url else None + return self._image_input(url) if url else None # Don't recognize it — skip it rather than guess. logger.warning("Ignoring unsupported input content type: %s", part_type) return None @@ -692,12 +692,17 @@ def _dispatch_dict_content_part( def _binary_as_image(self, part: BinaryInputContent) -> dict[str, Any] | None: if part.url: - return {"type": "input_image", "image_url": part.url} + return self._image_input(part.url) if part.data: mime = part.mime_type or "application/octet-stream" - return {"type": "input_image", "image_url": f"data:{mime};base64,{part.data}"} + return self._image_input(f"data:{mime};base64,{part.data}") return None + @staticmethod + def _image_input(url: str) -> dict[str, Any]: + """Build an OpenAI image input with the required detail level.""" + return {"type": "input_image", "image_url": url, "detail": "auto"} + def _binary_as_audio( self, part: BinaryInputContent, diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index cdb12cb2c6..c406633f92 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -76,8 +76,7 @@ class TranslatedInput(BaseModel): # ── Translated payload (the actual work the translator does) ──────── messages: list[TResponseInputItem] """Responses-API input items, ready to pass to Runner.run*(input=...). - Validation is skipped here because the SDK's own input types use forward - refs Pydantic can't resolve from this module.""" + Pydantic validates these against the SDK's input-item types.""" tools: SkipValidation[list[FunctionTool]] = [] """FunctionTool proxies for the client's tools. Merge these with your diff --git a/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py b/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py index d9f3ccea40..f6766a3c67 100644 --- a/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py +++ b/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py @@ -2,15 +2,15 @@ Tests for AGUIToOpenAITranslator's multimodal content mapping. Pins the wire shape each ``translate_*_content`` method produces (or the -``None``/drop behavior when unsupported), per the table in -``.dev/MESSAGES.md`` / ``.dev/INTEGRATIONS_MATRIX.md``. No network, no model; -just the mapping. +``None``/drop behavior when unsupported), and validates the final input against +the OpenAI Agents SDK types. No network or model is used. """ from __future__ import annotations import warnings +import pytest from ag_ui.core import ( AudioInputContent, BinaryInputContent, @@ -18,11 +18,16 @@ ImageInputContent, InputContentDataSource, InputContentUrlSource, + RunAgentInput, TextInputContent, UserMessage, VideoInputContent, ) +from pydantic import ValidationError + +from ag_ui_openai_agents import AGUITranslator from ag_ui_openai_agents.engine.agui_to_openai import AGUIToOpenAITranslator +from ag_ui_openai_agents.engine.types import TranslatedInput _engine = AGUIToOpenAITranslator() @@ -41,7 +46,11 @@ def _binary(**kwargs) -> BinaryInputContent: def test_image_url_source_passes_through(): part = ImageInputContent(source=InputContentUrlSource(value="https://x/y.png")) out = _engine.translate_image_content(part) - assert out == {"type": "input_image", "image_url": "https://x/y.png"} + assert out == { + "type": "input_image", + "image_url": "https://x/y.png", + "detail": "auto", + } def test_image_data_source_becomes_data_url(): @@ -49,7 +58,11 @@ def test_image_data_source_becomes_data_url(): source=InputContentDataSource(value="Zm9v", mime_type="image/png") ) out = _engine.translate_image_content(part) - assert out == {"type": "input_image", "image_url": "data:image/png;base64,Zm9v"} + assert out == { + "type": "input_image", + "image_url": "data:image/png;base64,Zm9v", + "detail": "auto", + } def test_image_with_no_usable_source_returns_none(): @@ -146,7 +159,11 @@ def test_video_is_always_dropped(): def test_binary_image_mime_routes_to_image_url(): part = _binary(mime_type="image/png", url="https://x/y.png") out = _engine.translate_binary_content(part) - assert out == {"type": "input_image", "image_url": "https://x/y.png"} + assert out == { + "type": "input_image", + "image_url": "https://x/y.png", + "detail": "auto", + } def test_binary_audio_mime_routes_to_input_audio(): @@ -171,13 +188,21 @@ def test_binary_other_mime_routes_to_file(): def test_binary_as_image_prefers_url_over_data(): part = _binary(mime_type="image/png", url="https://x/y.png", data="Zm9v") out = _engine._binary_as_image(part) - assert out == {"type": "input_image", "image_url": "https://x/y.png"} + assert out == { + "type": "input_image", + "image_url": "https://x/y.png", + "detail": "auto", + } def test_binary_as_image_falls_back_to_data(): part = _binary(mime_type="image/png", data="Zm9v") out = _engine._binary_as_image(part) - assert out == {"type": "input_image", "image_url": "data:image/png;base64,Zm9v"} + assert out == { + "type": "input_image", + "image_url": "data:image/png;base64,Zm9v", + "detail": "auto", + } def test_binary_as_image_with_neither_returns_none(): @@ -234,7 +259,11 @@ def test_dispatch_dict_content_part_image(): out = _engine._dispatch_dict_content_part( {"type": "image", "source": {"type": "url", "value": "https://x/y.png"}} ) - assert out == {"type": "input_image", "image_url": "https://x/y.png"} + assert out == { + "type": "input_image", + "image_url": "https://x/y.png", + "detail": "auto", + } def test_dispatch_dict_content_part_unknown_type_returns_none(): @@ -251,6 +280,7 @@ def test_translate_content_part_dispatches_by_type(): assert _engine.translate_content_part(image) == { "type": "input_image", "image_url": "https://x/y.png", + "detail": "auto", } @@ -305,8 +335,72 @@ def test_mixed_multimodal_parts_translate_to_matching_blocks(): blocks = [_engine.translate_content_part(p) for p in parts] assert blocks == [ {"type": "input_text", "text": "what's this?"}, - {"type": "input_image", "image_url": "https://x/y.png"}, + { + "type": "input_image", + "image_url": "https://x/y.png", + "detail": "auto", + }, {"type": "input_audio", "input_audio": {"data": "Zm9v", "format": "mp3"}}, {"type": "input_file", "file_url": "https://x/y.pdf"}, None, # video: no Responses-API input block ] + + +def test_image_survives_translated_input_validation(): + run_input = RunAgentInput( + thread_id="t1", + run_id="r1", + messages=[ + UserMessage( + id="u1", + role="user", + content=[ + ImageInputContent( + source=InputContentUrlSource(value="https://x/y.png") + ) + ], + ) + ], + tools=[], + state={}, + context=[], + forwarded_props=None, + ) + + translated = AGUITranslator().to_openai(run_input) + + assert translated.messages[0]["content"][0] == { + "type": "input_image", + "image_url": "https://x/y.png", + "detail": "auto", + } + + +def test_translated_input_rejects_image_without_detail(): + with pytest.raises(ValidationError) as exc_info: + TranslatedInput( + thread_id="t1", + run_id="r1", + messages=[ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_image", + "image_url": "https://x/y.png", + } + ], + } + ], + tools=[], + state={}, + context=[], + forwarded_props=None, + ) + + assert any( + error["loc"][-1] == "detail" + and "ResponseInputImageParam" in error["loc"] + for error in exc_info.value.errors() + ) From 0d982cec5de354cd50a6036fb317df2dfc926bba Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 12:44:54 +0200 Subject: [PATCH 76/94] feat(openai-agents): enhance placeholder ID management for reasoning events --- .../engine/openai_to_agui.py | 51 +++++++++---- .../tests/engine/test_openai_to_agui.py | 75 +++++++++++++++++++ 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index 3b04ca15d2..085bbb0b42 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -100,6 +100,10 @@ def __init__(self) -> None: self._reasoning_part_seq: dict[str, int] = {} # key -> next part index self._reasoning_phase_ids: dict[str, str] = {} # key -> phase message_id + # Active placeholder IDs: output_index -> unique internal key. + self._placeholder_window_keys: dict[Any, str] = {} + self._placeholder_window_seq = 0 + # Agent-step state for ordered STEP_FINISHED and STEP_STARTED events. self._current_step: str | None = None # active step name @@ -306,7 +310,7 @@ def translate_output_item_added(self, data: Any) -> list[BaseEvent]: item = read_attr(data, "item") item_type = read_attr(item, "type") item_id = read_attr(item, "id") - key = self._window_key(item_id, read_attr(data, "output_index")) + key = self._window_key(item_id, read_attr(data, "output_index"), start=True) if item_type == OpenAIItemType.MESSAGE: # Defer text until its first delta to avoid empty messages on tool-only @@ -344,20 +348,26 @@ def translate_output_item_done(self, data: Any) -> list[BaseEvent]: """ item = read_attr(data, "item") item_type = read_attr(item, "type") - key = self._window_key(read_attr(item, "id"), read_attr(data, "output_index")) + item_id = read_attr(item, "id") + output_index = read_attr(data, "output_index") + key = self._window_key(item_id, output_index) if item_type == OpenAIItemType.MESSAGE: # Discard a pending message if no text delta opened it; otherwise # _close_text closes the active sequence below. self._pending_text_ids.pop(key, None) - return self._close_text(key) - if item_type == OpenAIItemType.FUNCTION_CALL or item_type in HOSTED_TOOL_CALL_TYPES: - return self._close_tool_call(key) - if item_type == OpenAIItemType.REASONING: + events = self._close_text(key) + elif item_type == OpenAIItemType.FUNCTION_CALL or item_type in HOSTED_TOOL_CALL_TYPES: + events = self._close_tool_call(key) + elif item_type == OpenAIItemType.REASONING: events = self._emit_encrypted_value(key, item) events.extend(self._close_reasoning(key)) - return events - return [] + else: + events = [] + + if not self._is_real_id(item_id): + self._placeholder_window_keys.pop(output_index, None) + return events def translate_text_delta(self, data: Any) -> list[BaseEvent]: """Translate response.output_text.delta into TEXT_MESSAGE_CONTENT (lazy start). @@ -814,25 +824,36 @@ def _resolve_id(cls, item_id: Any, generate: Any) -> str: """ return item_id if cls._is_real_id(item_id) else generate() - @classmethod - def _window_key(cls, item_id: Any, output_index: Any) -> str: + def _window_key(self, item_id: Any, output_index: Any, *, start: bool = False) -> str: """Return the internal key that correlates one output item's raw events. This key indexes open text, tool-call, and reasoning sequences; it is never emitted as an AG-UI ID. A real OpenAI item ID is used when - available. Missing or placeholder IDs fall back to ``output_index``, - which is unique per item within a response. + available. Placeholder IDs use ``output_index`` only while that item + is active, then receive a new internal key when the index is reused. Args: item_id: The item's wire id, or None. output_index: The item's position in the response. + start: Whether this event starts a new output item. Returns: - The OpenAI item ID, or ``__idx_`` as a fallback. + The OpenAI item ID, or a unique internal placeholder key. """ - if cls._is_real_id(item_id): + if self._is_real_id(item_id): return item_id - return f"__idx_{output_index}" + key = self._placeholder_window_keys.get(output_index) + active = key is not None and ( + key in self._open_texts + or key in self._pending_text_ids + or key in self._open_tool_calls + or key in self._open_reasonings + ) + if key is None or (start and not active): + key = f"__idx_{output_index}_{self._placeholder_window_seq}" + self._placeholder_window_seq += 1 + self._placeholder_window_keys[output_index] = key + return key def _reconcile( self, diff --git a/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py index a3f8dbd61d..9a5c0a13e8 100644 --- a/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py +++ b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py @@ -25,6 +25,7 @@ MCPApprovalRequestItem, ) from agents.items import ToolCallItem, ToolCallOutputItem +from agents.models.fake_id import FAKE_RESPONSES_ID from openai.types.responses import ResponseFunctionToolCall from openai.types.responses.response_output_item import McpApprovalRequest @@ -304,6 +305,80 @@ def test_reasoning_summary_delta_opens_phase_and_part(): assert _types(engine.finalize()) == [EventType.REASONING_END] +def test_fake_reasoning_id_starts_fresh_when_output_index_is_reused(): + engine = OpenAIToAGUITranslator() + events = [] + + for encrypted in ("first", "second"): + events.extend( + _drive( + engine, + _added(OpenAIItemType.REASONING, id=FAKE_RESPONSES_ID), + _raw( + OpenAIRawResponseEventType.REASONING_TEXT_DELTA, + item_id=FAKE_RESPONSES_ID, + output_index=0, + delta="thinking", + ), + _raw( + OpenAIRawResponseEventType.REASONING_TEXT_DONE, + item_id=FAKE_RESPONSES_ID, + output_index=0, + ), + _done( + OpenAIItemType.REASONING, + id=FAKE_RESPONSES_ID, + encrypted_content=encrypted, + ), + ) + ) + + # A new item must also reset the key when the previous provider turn + # omitted output_item.done but another output already closed its reasoning. + events.extend( + _drive( + engine, + _added(OpenAIItemType.REASONING, id=FAKE_RESPONSES_ID), + _raw( + OpenAIRawResponseEventType.REASONING_TEXT_DELTA, + item_id=FAKE_RESPONSES_ID, + output_index=0, + delta="thinking", + ), + _raw( + OpenAIRawResponseEventType.REASONING_TEXT_DONE, + item_id=FAKE_RESPONSES_ID, + output_index=0, + ), + _added(OpenAIItemType.MESSAGE, output_index=1, id="msg_1"), + _added(OpenAIItemType.REASONING, id=FAKE_RESPONSES_ID), + _raw( + OpenAIRawResponseEventType.REASONING_TEXT_DELTA, + item_id=FAKE_RESPONSES_ID, + output_index=0, + delta="thinking again", + ), + ) + ) + + phase_ids = [ + event.message_id for event in events if event.type == EventType.REASONING_START + ] + part_ids = [ + event.message_id + for event in events + if event.type == EventType.REASONING_MESSAGE_START + ] + encrypted_values = [ + event.encrypted_value + for event in events + if event.type == EventType.REASONING_ENCRYPTED_VALUE + ] + + assert part_ids == phase_ids + assert encrypted_values == ["first", "second"] + + def test_reasoning_auto_closes_when_text_output_starts(): # Once real output begins, any open reasoning must be closed first — # reasoning must never bleed into the answer window. From cf1297fc9234621d3d6754c7aa9688e54aff9839 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 12:58:44 +0200 Subject: [PATCH 77/94] fix(openai-agents): close text on output_item.done, not output_text.don --- integrations/openai-agents/python/README.md | 10 +++ .../engine/openai_to_agui.py | 20 ++--- .../tests/engine/test_openai_to_agui.py | 78 ++++++++++++++++++- 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index 8a9fa59df7..d9cb0cfac6 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -620,6 +620,16 @@ Source of truth: `engine/agui_to_openai.py` (inbound) and | `MCPListToolsItem`, `MCPApprovalResponseItem` | *(dropped)* | Server-side bookkeeping; dropped with a debug log, no ID involved. | | Run input / stream completion / error | `RUN_STARTED`, then `RUN_FINISHED`/`RUN_ERROR`; optional `STATE_SNAPSHOT`; `MESSAGES_SNAPSHOT` by default | **`RunAgentInput.thread_id`/`run_id` → same fields on the lifecycle events** (passthrough, not generated). | +Text windows close on **item-level** signals only. `response.output_text.done` +ends one content *part*, not the message — a message can carry several parts +(multiple text blocks, or text followed by a refusal), so the translator +ignores it and keeps streaming later parts into the same window under the same +`message_id`. `TEXT_MESSAGE_END` is emitted by whichever of these arrives +first: `response.output_item.done`, the run-item commit (for backends that +skip the raw done), or `finalize()` (stream ended mid-message). Closing on the +part-level done would split one wire message into several AG-UI messages, the +later ones under generated IDs the snapshot could not merge. + #### ID rules that apply everywhere - **Real wire ID always wins.** Generated only when the SDK sends none, or diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index 085bbb0b42..1d65b1e327 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -382,23 +382,23 @@ def translate_text_delta(self, data: Any) -> list[BaseEvent]: return self._emit_text_content(key, read_attr(data, "delta") or "") def translate_text_done(self, data: Any) -> list[BaseEvent]: - """Translate response.output_text.done into an early TEXT_MESSAGE_END. + """Handle response.output_text.done without closing the message window. - Some model backends skip output_item.done; this closes the - window on the text-level done signal instead. Idempotent — - closing an already-closed window is a no-op. + This signal marks the end of one text content part, not the whole + assistant message — a message can carry several parts (multiple + output_text parts, or text followed by a refusal). Closing here + would split one wire message into several AG-UI messages, the + later ones under generated ids the snapshot can't merge. The + window closes on output_item.done instead, with the run-item + commit and finalize() as fallbacks when a backend skips it. Args: data: The raw output_text.done payload. Returns: - The closing event, or [] if already closed. + Always [] — the window outlives the content part. """ - key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) - # A text-level done with no delta ever seen: drop the deferred window - # the same way output_item.done does, so it never opens empty. - self._pending_text_ids.pop(key, None) - return self._close_text(key) + return [] def translate_refusal_delta(self, data: Any) -> list[BaseEvent]: """Translate response.refusal.delta into TEXT_MESSAGE_CONTENT. diff --git a/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py index 9a5c0a13e8..da4fba65ad 100644 --- a/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py +++ b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py @@ -24,9 +24,13 @@ HandoffOutputItem, MCPApprovalRequestItem, ) -from agents.items import ToolCallItem, ToolCallOutputItem +from agents.items import MessageOutputItem, ToolCallItem, ToolCallOutputItem from agents.models.fake_id import FAKE_RESPONSES_ID -from openai.types.responses import ResponseFunctionToolCall +from openai.types.responses import ( + ResponseFunctionToolCall, + ResponseOutputMessage, + ResponseOutputText, +) from openai.types.responses.response_output_item import McpApprovalRequest from ag_ui.core import EventType @@ -71,6 +75,19 @@ def _run_item(item) -> SimpleNamespace: return SimpleNamespace(type=OpenAIStreamEventType.RUN_ITEM, name="x", item=item) +def _message_item(item_id: str, text: str) -> MessageOutputItem: + return MessageOutputItem( + agent=_AGENT, + raw_item=ResponseOutputMessage( + id=item_id, + type="message", + role="assistant", + status="completed", + content=[ResponseOutputText(type="output_text", text=text, annotations=[])], + ), + ) + + def _agent_updated(name: str) -> SimpleNamespace: return SimpleNamespace( type=OpenAIStreamEventType.AGENT_UPDATED, @@ -126,7 +143,9 @@ def test_text_delta_lazily_opens_window_without_output_item_added(): assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] -def test_text_done_closes_window_when_output_item_done_is_skipped(): +def test_text_done_does_not_close_the_message_window(): + # output_text.done ends one content part, not the message — the window + # must stay open for output_item.done to close. engine = OpenAIToAGUITranslator() events = _drive( engine, @@ -134,7 +153,60 @@ def test_text_done_closes_window_when_output_item_done_is_skipped(): _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), _raw(OpenAIRawResponseEventType.TEXT_DONE, item_id="msg_1", output_index=0), ) + assert EventType.TEXT_MESSAGE_END not in _types(events) + events = _drive(engine, _done(OpenAIItemType.MESSAGE, id="msg_1")) + assert _types(events) == [EventType.TEXT_MESSAGE_END] + assert events[0].message_id == "msg_1" + + +def test_two_text_parts_stream_as_one_message(): + # A message can carry several content parts, each with its own + # output_text.done. All of them belong to one AG-UI message id. + engine = OpenAIToAGUITranslator() + events = _drive( + engine, + _added(OpenAIItemType.MESSAGE, id="msg_1"), + _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="First"), + _raw(OpenAIRawResponseEventType.TEXT_DONE, item_id="msg_1", output_index=0), + _raw( + OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="Second" + ), + _raw(OpenAIRawResponseEventType.TEXT_DONE, item_id="msg_1", output_index=0), + _done(OpenAIItemType.MESSAGE, id="msg_1"), + ) + assert _types(events) == [ + EventType.TEXT_MESSAGE_START, + EventType.TEXT_MESSAGE_CONTENT, + EventType.TEXT_MESSAGE_CONTENT, + EventType.TEXT_MESSAGE_END, + ] + assert {e.message_id for e in events} == {"msg_1"} + + +def test_run_item_commit_closes_text_when_output_item_done_is_skipped(): + engine = OpenAIToAGUITranslator() + events = _drive( + engine, + _added(OpenAIItemType.MESSAGE, id="msg_1"), + _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), + _raw(OpenAIRawResponseEventType.TEXT_DONE, item_id="msg_1", output_index=0), + _run_item(_message_item("msg_1", "hi")), + ) assert _types(events)[-1] == EventType.TEXT_MESSAGE_END + assert events[-1].message_id == "msg_1" + + +def test_finalize_closes_text_left_open_after_text_done(): + engine = OpenAIToAGUITranslator() + _drive( + engine, + _added(OpenAIItemType.MESSAGE, id="msg_1"), + _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), + _raw(OpenAIRawResponseEventType.TEXT_DONE, item_id="msg_1", output_index=0), + ) + events = engine.finalize() + assert _types(events) == [EventType.TEXT_MESSAGE_END] + assert events[0].message_id == "msg_1" def test_refusal_delta_streams_into_the_text_window(): From 3b63fa14acb57c2700259834b736eb672d545bff Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 13:22:23 +0200 Subject: [PATCH 78/94] refactor(openai-agents): simplify docs copilot retrieval to read-by-heading --- .../(v2)/ag_ui_docs_copilot/README.mdx | 29 +-- .../feature/(v2)/ag_ui_docs_copilot/page.tsx | 85 ++++----- apps/dojo/src/files.json | 10 +- .../openai-agents/python/examples/README.md | 27 ++- .../agents_examples/ag_ui_docs_copilot.py | 172 ++++++++---------- .../examples/agents_examples/docs_search.py | 143 --------------- .../examples/tests/test_ag_ui_docs_copilot.py | 67 +++---- 7 files changed, 185 insertions(+), 348 deletions(-) delete mode 100644 integrations/openai-agents/python/examples/agents_examples/docs_search.py diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx index 2e026bf346..a3dea9a764 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx @@ -3,36 +3,39 @@ This example is a small documentation assistant for the AG-UI integration with the OpenAI Agents SDK. -The main **Copilot** handles normal conversation without loading the -documentation. For AG-UI documentation or code questions, it calls a second -OpenAI Agents SDK agents, **AG-UI OpenAI Agents Specialist** and **AG-UI -Protocol Python Specialist**, through -`Agent.as_tool()`. Each specialist receives only its relevant local README. +One **Copilot** agent handles normal conversation directly, and for AG-UI or +OpenAI Agents SDK questions calls one of two tools, `read_ag_ui_protocol_docs` +and `read_ag_ui_openai_agents_docs`, to read a single section of the relevant +local README by heading. ## What it demonstrates ```text -Copilot → OpenAI Agents / Protocol specialists (local READMEs) +Copilot → read_ag_ui_protocol_docs / read_ag_ui_openai_agents_docs (local READMEs, one section at a time) ↓ RunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE ``` - Local documentation with no internet request or retrieval framework -- An OpenAI Agents SDK documentation specialist used as a tool by Copilot +- Table-of-contents instructions plus an on-demand section-read tool, instead + of loading a whole README into the prompt - A direct, visible AG-UI translator endpoint - Streaming lifecycle, text, and tool-call events to CopilotKit -## Why the documentation is loaded directly +## Why the documentation is loaded by section -The integration README is small enough for this focused demo, so it is read -once and included in the agents' instructions. This keeps the example -deterministic and easy to copy. A production assistant with many or large -documents should replace this with its own retrieval system. +The integration README is small enough for this focused demo, so its heading +list sits in the Copilot's instructions and each tool call reads back one +matching section. This keeps the example deterministic, cheap, and fast to +stream — an earlier version routed each question through a second +sub-agent, which cost extra model round trips before the first token showed +up. A production assistant with many or large documents should replace this +with its own retrieval system. ## Try it - Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and SDK. -- Ask the OpenAI Agents Specialist to generate a minimal FastAPI streaming endpoint. +- Ask for a minimal FastAPI streaming endpoint. - Ask which translator options control lifecycle events, snapshots, state, and errors. diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx index b21fc03bea..e67949eec3 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx @@ -30,27 +30,27 @@ const AGUIDocsCopilot: React.FC = ({ params }) => { const DocsChat = () => { useRenderTool({ - name: "ask_ag_ui_openai_agents_docs", + name: "read_ag_ui_openai_agents_docs", agentId: "ag_ui_docs_copilot", - parameters: z.object({ input: z.string().optional() }), + parameters: z.object({ heading: z.string().optional() }), render: ({ status, args, result }: any) => ( ), }); useRenderTool({ - name: "ask_ag_ui_protocol_docs", + name: "read_ag_ui_protocol_docs", agentId: "ag_ui_docs_copilot", - parameters: z.object({ input: z.string().optional() }), + parameters: z.object({ heading: z.string().optional() }), render: ({ status, args, result }: any) => ( ), @@ -92,54 +92,55 @@ const DocsChat = () => { function DocsLookupProgress({ source, status, - query, + heading, result, }: { source: "AG-UI OpenAI Agents" | "AG-UI Protocol"; status: "inProgress" | "executing" | "complete"; - query?: string; + heading?: string; result?: string; }) { const complete = status === "complete"; - const step = complete - ? "Answer ready" - : status === "executing" - ? "Finding the relevant section" - : `Opening the ${source} guide`; + const missed = complete && !!result && result.startsWith("No section matches"); return ( -
-
- - {complete ? "✓" : "📚"} - - - {complete ? `${source} docs consulted` : `Consulting ${source} docs`} - - {!complete && ( - - - - +
+ + {missed ? "!" : complete ? "✓" : "📖"} + +
+
+ + {source} docs - )} -
-
- {step} -
- {!complete && query && ( -
- {query} + {!complete && ( + + + + • + + + • + + + )}
- )} - {complete && result && ( -
- {source} specialist finished. +
+ {heading + ? `${complete ? "Read" : "Reading"} "${heading}"` + : complete + ? "Section read" + : "Opening the guide"}
- )} +
); } diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index f5d303b776..dd2f788235 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4816,19 +4816,19 @@ "openai-agents-python::ag_ui_docs_copilot": [ { "name": "page.tsx", - "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"ask_ag_ui_openai_agents_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n useRenderTool({\n name: \"ask_ag_ui_protocol_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n source,\n status,\n query,\n result,\n}: {\n source: \"AG-UI OpenAI Agents\" | \"AG-UI Protocol\";\n status: \"inProgress\" | \"executing\" | \"complete\";\n query?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const step = complete\n ? \"Answer ready\"\n : status === \"executing\"\n ? \"Finding the relevant section\"\n : `Opening the ${source} guide`;\n\n return (\n
\n
\n \n {complete ? \"✓\" : \"📚\"}\n \n \n {complete ? `${source} docs consulted` : `Consulting ${source} docs`}\n \n {!complete && (\n \n \n \n \n \n )}\n
\n
\n {step}\n
\n {!complete && query && (\n
\n {query}\n
\n )}\n {complete && result && (\n
\n {source} specialist finished.\n
\n )}\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", + "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"read_ag_ui_openai_agents_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ heading: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n useRenderTool({\n name: \"read_ag_ui_protocol_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ heading: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n source,\n status,\n heading,\n result,\n}: {\n source: \"AG-UI OpenAI Agents\" | \"AG-UI Protocol\";\n status: \"inProgress\" | \"executing\" | \"complete\";\n heading?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const missed = complete && !!result && result.startsWith(\"No section matches\");\n\n return (\n
\n \n {missed ? \"!\" : complete ? \"✓\" : \"📖\"}\n \n
\n
\n \n {source} docs\n \n {!complete && (\n \n \n \n •\n \n \n •\n \n \n )}\n
\n
\n {heading\n ? `${complete ? \"Read\" : \"Reading\"} \"${heading}\"`\n : complete\n ? \"Section read\"\n : \"Opening the guide\"}\n
\n
\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nThe main **Copilot** handles normal conversation without loading the\ndocumentation. For AG-UI documentation or code questions, it calls a second\nOpenAI Agents SDK agents, **AG-UI OpenAI Agents Specialist** and **AG-UI\nProtocol Python Specialist**, through\n`Agent.as_tool()`. Each specialist receives only its relevant local README.\n\n## What it demonstrates\n\n```text\nCopilot → OpenAI Agents / Protocol specialists (local READMEs)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- An OpenAI Agents SDK documentation specialist used as a tool by Copilot\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded directly\n\nThe integration README is small enough for this focused demo, so it is read\nonce and included in the agents' instructions. This keeps the example\ndeterministic and easy to copy. A production assistant with many or large\ndocuments should replace this with its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask the OpenAI Agents Specialist to generate a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", + "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nOne **Copilot** agent handles normal conversation directly, and for AG-UI or\nOpenAI Agents SDK questions calls one of two tools, `read_ag_ui_protocol_docs`\nand `read_ag_ui_openai_agents_docs`, to read a single section of the relevant\nlocal README by heading.\n\n## What it demonstrates\n\n```text\nCopilot → read_ag_ui_protocol_docs / read_ag_ui_openai_agents_docs (local READMEs, one section at a time)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- Table-of-contents instructions plus an on-demand section-read tool, instead\n of loading a whole README into the prompt\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded by section\n\nThe integration README is small enough for this focused demo, so its heading\nlist sits in the Copilot's instructions and each tool call reads back one\nmatching section. This keeps the example deterministic, cheap, and fast to\nstream — an earlier version routed each question through a second\nsub-agent, which cost extra model round trips before the first token showed\nup. A production assistant with many or large documents should replace this\nwith its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask for a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", "language": "markdown", "type": "file" }, { "name": "ag_ui_docs_copilot.py", - "content": "\"\"\"AG-UI documentation assistant with two focused specialists as tools.\"\"\"\n\nfrom __future__ import annotations\n\nfrom importlib.metadata import metadata\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\nfrom .docs_search import MarkdownSearchIndex\n\n\ndef _load_distribution_readme(distribution: str) -> str:\n \"\"\"Load the installed package README from its distribution metadata.\"\"\"\n document = metadata(distribution).get_payload()\n if not isinstance(document, str) or not document.strip():\n raise RuntimeError(f\"{distribution} has no README in its package metadata\")\n return document\n\n\n# ag-ui-protocol specialist: knows the core Python SDK documentation.\nAG_UI_PROTOCOL_DOCS = _load_distribution_readme(\"ag-ui-protocol\")\n_AG_UI_PROTOCOL_DOCS_INDEX = MarkdownSearchIndex(AG_UI_PROTOCOL_DOCS)\n\n\n@function_tool\ndef search_ag_ui_protocol_docs(query: str) -> str:\n \"\"\"Find relevant AG-UI Protocol Python documentation sections.\n\n Args:\n query: The documentation question or API concepts to find.\n \"\"\"\n return _AG_UI_PROTOCOL_DOCS_INDEX.search(query)\n\n\nAG_UI_PROTOCOL_DOCS_INSTRUCTIONS = \"\"\"You are the technical specialist for\nAG-UI Protocol's core Python SDK. Help developers understand ag_ui.core data\nmodels, RunAgentInput, messages, events, event types, and EventEncoder.\n\nBefore answering, always call search_ag_ui_protocol_docs with a focused query.\nTreat its excerpts as your only source of truth. If the first search is\ninsufficient, search once more with different API terms.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial or unrelated integration\ndetails. Be concise and practical. For code requests, provide only the\nsmallest complete, production-readable Python snippet needed for the request,\nfollowed by a short explanation. Use documented APIs only. If the\ndocumentation does not establish an answer, say so briefly instead of\nguessing.\n\"\"\"\n\nag_ui_protocol_docs_agent = Agent(\n name=\"AG-UI Protocol Python Specialist\",\n model=DEFAULT_MODEL,\n instructions=AG_UI_PROTOCOL_DOCS_INSTRUCTIONS,\n tools=[search_ag_ui_protocol_docs],\n)\n\n\n# ag-ui-openai-agents specialist: knows the integration documentation.\nAG_UI_OPENAI_AGENTS_DOCS = _load_distribution_readme(\"ag-ui-openai-agents\")\n_AG_UI_OPENAI_AGENTS_DOCS_INDEX = MarkdownSearchIndex(AG_UI_OPENAI_AGENTS_DOCS)\n\n\n@function_tool\ndef search_ag_ui_openai_agents_docs(query: str) -> str:\n \"\"\"Find relevant AG-UI OpenAI Agents integration documentation sections.\n\n Args:\n query: The documentation question or integration concepts to find.\n \"\"\"\n return _AG_UI_OPENAI_AGENTS_DOCS_INDEX.search(query)\n\n\nAG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS = \"\"\"You are the technical specialist for\nthe AG-UI OpenAI Agents integration. Help developers understand the\nAGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE\nendpoints, tools, client tools, context, state, lifecycle events, errors, and\ntesting.\n\nBefore answering, always call search_ag_ui_openai_agents_docs with a focused\nquery. Treat its excerpts as your only source of truth. If the first search is\ninsufficient, search once more with different API terms.\n\nAnswer only the user's question. Retrieve and explain only the relevant parts\nof the documentation; do not add a broad tutorial, related features, or extra\noptions unless the user asks. Be concise and practical. For code requests,\nprovide only the smallest complete, production-readable Python snippet needed\nfor the request, followed by a short explanation. Use documented APIs only. Do\nnot invent behavior or configuration. If the documentation does not establish\nan answer, say so briefly instead of guessing.\n\"\"\"\n\nag_ui_openai_agents_docs_agent = Agent(\n name=\"AG-UI OpenAI Agents Specialist\",\n model=DEFAULT_MODEL,\n instructions=AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS,\n tools=[search_ag_ui_openai_agents_docs],\n)\n\n\n# Main Copilot: handles normal conversation and delegates documentation work.\ncopilot_instructions = \"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question or request about AG-UI, the OpenAI Agents SDK, translator\nAPIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle\nevents, errors, tests, or Python implementation, call the relevant specialist\nbefore answering. Use ask_ag_ui_openai_agents_docs for the OpenAI Agents SDK\nintegration and ask_ag_ui_protocol_docs for ag_ui.core protocol types and\nEventEncoder questions.\nUse only the part of the specialist's result that answers the user's question.\nDo not invent behavior or claim details a specialist did not provide. For code\nrequests, make sure the relevant specialist is called.\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[\n ag_ui_protocol_docs_agent.as_tool(\n tool_name=\"ask_ag_ui_protocol_docs\",\n tool_description=(\n \"Provide authoritative AG-UI core Python SDK guidance about \"\n \"protocol types, RunAgentInput, events, and EventEncoder.\"\n ),\n ),\n ag_ui_openai_agents_docs_agent.as_tool(\n tool_name=\"ask_ag_ui_openai_agents_docs\",\n tool_description=(\n \"Provide authoritative AG-UI and OpenAI Agents SDK guidance, \"\n \"including documented Python integration snippets.\"\n ),\n ),\n ],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", + "content": "\"\"\"AG-UI documentation assistant that reads local README sections on demand.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom importlib.metadata import metadata\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n_HEADING_RE = re.compile(r\"^#{1,6}\\s+(.+)$\")\n\n\ndef _load_distribution_readme(distribution: str) -> str:\n \"\"\"Load the installed package README from its distribution metadata.\"\"\"\n document = metadata(distribution).get_payload()\n if not isinstance(document, str) or not document.strip():\n raise RuntimeError(f\"{distribution} has no README in its package metadata\")\n return document\n\n\nclass MarkdownSections:\n \"\"\"A Markdown document split by heading, read one section at a time.\n\n The Copilot sees only the heading list in its instructions and fetches\n the sections it needs on demand, so the full document never sits in a\n prompt.\n \"\"\"\n\n def __init__(self, document: str) -> None:\n sections: dict[str, list[str]] = {}\n heading = \"Overview\"\n for line in document.splitlines():\n match = _HEADING_RE.match(line)\n if match:\n heading = match.group(1).strip()\n sections.setdefault(heading, []).append(line)\n self._sections = {\n name: content\n for name, lines in sections.items()\n if (content := \"\\n\".join(lines).strip())\n }\n\n @property\n def headings(self) -> str:\n \"\"\"The table of contents: one heading per line.\"\"\"\n return \"\\n\".join(f\"- {name}\" for name in self._sections)\n\n def read(self, heading: str) -> str:\n \"\"\"Return one section by heading (case-insensitive, substring ok).\"\"\"\n wanted = heading.strip().lstrip(\"#\").strip().lower()\n for name, content in self._sections.items():\n if name.lower() == wanted:\n return content\n for name, content in self._sections.items():\n if wanted in name.lower():\n return content\n return f\"No section matches {heading!r}. Available headings:\\n{self.headings}\"\n\n\n# ag-ui-protocol docs: the core Python SDK README.\nAG_UI_PROTOCOL_DOCS = _load_distribution_readme(\"ag-ui-protocol\")\n_AG_UI_PROTOCOL_SECTIONS = MarkdownSections(AG_UI_PROTOCOL_DOCS)\n\n\n@function_tool\ndef read_ag_ui_protocol_docs(heading: str) -> str:\n \"\"\"Read one AG-UI Protocol Python documentation section by its heading.\n\n Args:\n heading: A heading from the \"AG-UI Protocol docs\" table of contents\n in your instructions.\n \"\"\"\n return _AG_UI_PROTOCOL_SECTIONS.read(heading)\n\n\n# ag-ui-openai-agents docs: this integration's README.\nAG_UI_OPENAI_AGENTS_DOCS = _load_distribution_readme(\"ag-ui-openai-agents\")\n_AG_UI_OPENAI_AGENTS_SECTIONS = MarkdownSections(AG_UI_OPENAI_AGENTS_DOCS)\n\n\n@function_tool\ndef read_ag_ui_openai_agents_docs(heading: str) -> str:\n \"\"\"Read one AG-UI OpenAI Agents integration documentation section.\n\n Args:\n heading: A heading from the \"AG-UI OpenAI Agents docs\" table of\n contents in your instructions.\n \"\"\"\n return _AG_UI_OPENAI_AGENTS_SECTIONS.read(heading)\n\n\ncopilot_instructions = f\"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question about AG-UI, the OpenAI Agents SDK, translator APIs, FastAPI\nendpoints, streaming, tools, client tools, state, lifecycle events, errors,\ntests, or Python implementation, pick the most relevant heading from the\nmatching table of contents below and call the matching tool before answering.\nUse read_ag_ui_openai_agents_docs for OpenAI Agents SDK integration questions\nand read_ag_ui_protocol_docs for ag_ui.core protocol types and EventEncoder\nquestions. Treat the returned section as your only source of truth. Read\nanother section if the first one is not enough.\n\nUse only the part of the section that answers the user's question. Do not\ninvent behavior or claim details the documentation did not provide. For code\nrequests, provide only the smallest complete, production-readable Python\nsnippet needed, followed by a short explanation. Use documented APIs only.\n\nAG-UI Protocol docs table of contents:\n{_AG_UI_PROTOCOL_SECTIONS.headings}\n\nAG-UI OpenAI Agents docs table of contents:\n{_AG_UI_OPENAI_AGENTS_SECTIONS.headings}\n\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[read_ag_ui_protocol_docs, read_ag_ui_openai_agents_docs],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", "language": "python", "type": "file" } @@ -4902,7 +4902,7 @@ "openai-agents-python::human_in_the_loop_approval": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useCopilotKit,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface ApprovalProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface PendingApproval {\n callId: string;\n toolName: string;\n arguments: string;\n}\n\nconst Approval: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"human_in_the_loop_approval\" });\n const { copilotkit } = useCopilotKit();\n const [pending, setPending] = useState(null);\n const [resolvedCallId, setResolvedCallId] = useState(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Refund an order\", message: \"I'd like a refund for ORD-1001.\" },\n { title: \"Refund another order\", message: \"Please refund ORD-1002.\" },\n { title: \"Unknown order\", message: \"Can you refund ORD-9999?\" },\n ],\n available: \"always\",\n consumerAgentId: \"human_in_the_loop_approval\",\n });\n\n React.useEffect(() => {\n const subscription = agent.subscribe({\n onCustomEvent: ({ event }) => {\n if (event.name !== \"approval_request\") return;\n // One CustomEvent can carry several pending calls if the model made\n // more than one needs_approval call in a turn; this demo only ever\n // triggers one, so showing the first is enough here.\n const pendingList = event.value as Array<{\n call_id: string;\n tool_name: string;\n arguments: string;\n }>;\n const first = pendingList[0];\n if (!first) return;\n setResolvedCallId(null);\n setPending({\n callId: first.call_id,\n toolName: first.tool_name,\n arguments: first.arguments,\n });\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n const respond = (approve: boolean) => {\n if (!pending) return;\n setResolvedCallId(pending.callId);\n copilotkit.runAgent({\n agent,\n forwardedProps: {\n approval: { call_id: pending.callId, approve },\n },\n });\n setPending(null);\n };\n\n return (\n
\n {pending && (\n respond(true)} onReject={() => respond(false)} />\n )}\n {!pending && resolvedCallId && (\n
Decision sent — waiting for the agent...
\n )}\n
\n \n
\n
\n );\n};\n\nfunction ApprovalCard({\n pending,\n onApprove,\n onReject,\n}: {\n pending: PendingApproval;\n onApprove: () => void;\n onReject: () => void;\n}) {\n let args: Record = {};\n try {\n args = JSON.parse(pending.arguments);\n } catch {\n // leave args empty — raw string shown below is enough context either way\n }\n\n return (\n \n

Approval needed

\n

\n The agent wants to call {pending.toolName} with:\n

\n
\n        {JSON.stringify(args, null, 2)}\n      
\n
\n \n Reject\n \n \n Approve\n \n
\n
\n );\n}\n\nexport default Approval;\n", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useCopilotKit,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface ApprovalProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface PendingApproval {\n callId: string;\n toolName: string;\n arguments: string;\n}\n\nconst Approval: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"human_in_the_loop_approval\" });\n const { copilotkit } = useCopilotKit();\n const [pending, setPending] = useState(null);\n const [resolvedCallId, setResolvedCallId] = useState(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Refund an order\", message: \"I'd like a refund for ORD-1001.\" },\n { title: \"Refund another order\", message: \"Please refund ORD-1002.\" },\n { title: \"Unknown order\", message: \"Can you refund ORD-9999?\" },\n ],\n available: \"always\",\n consumerAgentId: \"human_in_the_loop_approval\",\n });\n\n React.useEffect(() => {\n const subscription = agent.subscribe({\n onCustomEvent: ({ event }) => {\n if (event.name !== \"approval_request\") return;\n // One CustomEvent can carry several pending calls if the model made\n // more than one needs_approval call in a turn; this demo only ever\n // triggers one, so showing the first is enough here.\n const pendingList = event.value as Array<{\n call_id: string;\n tool_name: string;\n arguments: string;\n }>;\n const first = pendingList[0];\n if (!first) return;\n setResolvedCallId(null);\n setPending({\n callId: first.call_id,\n toolName: first.tool_name,\n arguments: first.arguments,\n });\n },\n onRunFinishedEvent: () => setResolvedCallId(null),\n onRunErrorEvent: () => setResolvedCallId(null),\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n const respond = (approve: boolean) => {\n if (!pending) return;\n setResolvedCallId(pending.callId);\n copilotkit.runAgent({\n agent,\n forwardedProps: {\n approval: { call_id: pending.callId, approve },\n },\n });\n setPending(null);\n };\n\n return (\n
\n {pending && (\n respond(true)} onReject={() => respond(false)} />\n )}\n {!pending && resolvedCallId && (\n
Decision sent — waiting for the agent...
\n )}\n
\n \n
\n
\n );\n};\n\nfunction ApprovalCard({\n pending,\n onApprove,\n onReject,\n}: {\n pending: PendingApproval;\n onApprove: () => void;\n onReject: () => void;\n}) {\n let args: Record = {};\n try {\n args = JSON.parse(pending.arguments);\n } catch {\n // leave args empty — raw string shown below is enough context either way\n }\n\n return (\n \n

Approval needed

\n

\n The agent wants to call {pending.toolName} with:\n

\n
\n        {JSON.stringify(args, null, 2)}\n      
\n
\n \n Reject\n \n \n Approve\n \n
\n
\n );\n}\n\nexport default Approval;\n", "language": "typescript", "type": "file" }, @@ -4948,7 +4948,7 @@ "openai-agents-python::subagents": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useConfigureSuggestions, useRenderTool } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { z } from \"zod\";\n\ninterface SubagentsProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ntype Role = \"research\" | \"writer\" | \"critic\";\ntype Status = \"inProgress\" | \"executing\" | \"complete\";\n\nconst ROLES: { role: Role; label: string; emoji: string }[] = [\n { role: \"research\", label: \"Researcher\", emoji: \"🔎\" },\n { role: \"writer\", label: \"Writer\", emoji: \"✍️\" },\n { role: \"critic\", label: \"Critic\", emoji: \"🧐\" },\n];\n\ninterface DelegationEntry {\n id: string;\n role: Role;\n status: Status;\n text: string;\n}\n\nconst Subagents: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst SubagentsView = () => {\n const [active, setActive] = useState>({\n research: null,\n writer: null,\n critic: null,\n });\n const [log, setLog] = useState([]);\n\n // Called by each DelegationTracker instance as its tool call's status\n // changes. Same shared state feeds both the role chips (who's working\n // right now) and the log (what happened, in order).\n const track = useCallback((entry: DelegationEntry) => {\n setActive((prev) => ({\n ...prev,\n [entry.role]: entry.status === \"complete\" ? null : entry.status,\n }));\n setLog((prev) => {\n const idx = prev.findIndex((e) => e.id === entry.id);\n if (idx === -1) return [...prev, entry];\n const next = [...prev];\n next[idx] = entry;\n return next;\n });\n }, []);\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"History of AI\",\n message: \"Write a short article about the history of AI\",\n },\n {\n title: \"History of programming languages\",\n message: \"Write a short piece about the history of programming languages\",\n },\n ],\n available: \"always\",\n });\n\n useRenderTool({\n name: \"research_topic\",\n agentId: \"subagents\",\n parameters: z.object({ topic: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"write_prose\",\n agentId: \"subagents\",\n parameters: z.object({ facts: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"critique_draft\",\n agentId: \"subagents\",\n parameters: z.object({ draft: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n return (\n
\n
\n

Supervisor's team

\n
\n {ROLES.map(({ role, label, emoji }) => {\n const status = active[role];\n return (\n \n {emoji}\n {label}\n {status && (\n \n {status === \"executing\" ? \"working\" : \"starting\"}\n \n )}\n
\n );\n })}\n
\n\n

\n Delegation log\n

\n
\n {log.length === 0 && (\n
No delegations yet.
\n )}\n {log.map((entry) => {\n const roleInfo = ROLES.find((r) => r.role === entry.role);\n return (\n \n
\n \n {roleInfo?.emoji} {roleInfo?.label}\n \n \n {entry.status}\n \n
\n {entry.text && (\n
\n {entry.text}\n
\n )}\n
\n );\n })}\n
\n
\n\n
\n
\n \n
\n
\n \n );\n};\n\n// Each tool call renders its own instance of this. `render` is invoked as a\n// real component (not a plain function), so effects are legal here — this\n// is what reports status upstream into the shared chip/log state on every\n// inProgress → executing → complete transition, in addition to rendering\n// its own inline card in the chat transcript.\nfunction DelegationTracker({\n role,\n toolCallId,\n status,\n preview,\n result,\n onUpdate,\n}: {\n role: Role;\n toolCallId: string;\n status: Status;\n preview?: string;\n result?: string;\n onUpdate: (entry: DelegationEntry) => void;\n}) {\n const text = status === \"complete\" ? (result ?? \"\") : (preview ?? \"\");\n\n useEffect(() => {\n onUpdate({ id: toolCallId, role, status, text });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [toolCallId, role, status, text]);\n\n const roleInfo = ROLES.find((r) => r.role === role)!;\n const label =\n status === \"complete\"\n ? `${roleInfo.label} finished`\n : status === \"executing\"\n ? `${roleInfo.label} working…`\n : `Calling ${roleInfo.label.toLowerCase()}…`;\n\n return (\n
\n {roleInfo.emoji} {label}\n
\n );\n}\n\nexport default Subagents;\n", + "content": "\"use client\";\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useConfigureSuggestions, useRenderTool } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { z } from \"zod\";\n\ninterface SubagentsProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ntype Role = \"research\" | \"writer\" | \"critic\";\ntype Status = \"inProgress\" | \"executing\" | \"complete\";\n\nconst ROLES: { role: Role; label: string; emoji: string }[] = [\n { role: \"research\", label: \"Researcher\", emoji: \"🔎\" },\n { role: \"writer\", label: \"Writer\", emoji: \"✍️\" },\n { role: \"critic\", label: \"Critic\", emoji: \"🧐\" },\n];\n\ninterface DelegationEntry {\n id: string;\n role: Role;\n status: Status;\n text: string;\n}\n\nconst Subagents: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst SubagentsView = () => {\n const [active, setActive] = useState>({\n research: null,\n writer: null,\n critic: null,\n });\n const [log, setLog] = useState([]);\n\n // Called by each DelegationTracker instance as its tool call's status\n // changes. Same shared state feeds both the role chips (who's working\n // right now) and the log (what happened, in order).\n const track = useCallback((entry: DelegationEntry) => {\n setActive((prev) => ({\n ...prev,\n [entry.role]: entry.status === \"complete\" ? null : entry.status,\n }));\n setLog((prev) => {\n const idx = prev.findIndex((e) => e.id === entry.id);\n if (idx === -1) return [...prev, entry];\n const next = [...prev];\n next[idx] = entry;\n return next;\n });\n }, []);\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"History of AI\",\n message: \"Write a short article about the history of AI\",\n },\n {\n title: \"History of programming languages\",\n message: \"Write a short piece about the history of programming languages\",\n },\n ],\n available: \"always\",\n });\n\n useRenderTool({\n name: \"research_topic\",\n agentId: \"subagents\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"write_prose\",\n agentId: \"subagents\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"critique_draft\",\n agentId: \"subagents\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n return (\n
\n
\n

Supervisor's team

\n
\n {ROLES.map(({ role, label, emoji }) => {\n const status = active[role];\n return (\n \n {emoji}\n {label}\n {status && (\n \n {status === \"executing\" ? \"working\" : \"starting\"}\n \n )}\n
\n );\n })}\n
\n\n

\n Delegation log\n

\n
\n {log.length === 0 && (\n
No delegations yet.
\n )}\n {log.map((entry) => {\n const roleInfo = ROLES.find((r) => r.role === entry.role);\n return (\n \n
\n \n {roleInfo?.emoji} {roleInfo?.label}\n \n \n {entry.status}\n \n
\n {entry.text && (\n
\n {entry.text}\n
\n )}\n
\n );\n })}\n
\n \n\n
\n
\n \n
\n
\n \n );\n};\n\n// Each tool call renders its own instance of this. `render` is invoked as a\n// real component (not a plain function), so effects are legal here — this\n// is what reports status upstream into the shared chip/log state on every\n// inProgress → executing → complete transition, in addition to rendering\n// its own inline card in the chat transcript.\nfunction DelegationTracker({\n role,\n toolCallId,\n status,\n preview,\n result,\n onUpdate,\n}: {\n role: Role;\n toolCallId: string;\n status: Status;\n preview?: string;\n result?: string;\n onUpdate: (entry: DelegationEntry) => void;\n}) {\n const text = status === \"complete\" ? (result ?? \"\") : (preview ?? \"\");\n\n useEffect(() => {\n onUpdate({ id: toolCallId, role, status, text });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [toolCallId, role, status, text]);\n\n const roleInfo = ROLES.find((r) => r.role === role)!;\n const label =\n status === \"complete\"\n ? `${roleInfo.label} finished`\n : status === \"executing\"\n ? `${roleInfo.label} working…`\n : `Calling ${roleInfo.label.toLowerCase()}…`;\n\n return (\n
\n {roleInfo.emoji} {label}\n
\n );\n}\n\nexport default Subagents;\n", "language": "typescript", "type": "file" }, diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 1482272864..7d731794ce 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -3,10 +3,9 @@ Runnable demos for `ag_ui_openai_agents`, one mounted FastAPI app per agent. The aggregate server mounts each example application: -- **`ag_ui_docs_copilot`** handles normal conversation with a small main - Copilot and delegates integration questions to an - `AG-UI OpenAI Agents Specialist` and core protocol questions to an - `AG-UI Protocol Python Specialist` as tools. +- **`ag_ui_docs_copilot`** handles normal conversation directly and reads + local README sections on demand via `read_ag_ui_openai_agents_docs` and + `read_ag_ui_protocol_docs` tools. - The remaining focused feature apps use **`OpenAIAgentsAgent`** and `add_openai_agents_fastapi_endpoint` where their run does not require custom control. @@ -67,18 +66,18 @@ lists every registered agent. Demos map 1:1 onto the AG-UI Dojo feature pages. ### `ag_ui_docs_copilot` -The main Copilot handles normal conversation without carrying the documentation -in its instructions. Documentation and code questions are delegated to an -`AG-UI OpenAI Agents Specialist` and `AG-UI Protocol Python Specialist` through -the SDK's `Agent.as_tool()` API. Each specialist searches a local Markdown -index first, so only the highest-ranked README sections enter the model context -instead of the complete documentation. The retrieval is deterministic and adds -no vector database, embedding request, or network dependency. The endpoint -keeps the direct `to_openai` → `Runner.run_streamed` → `to_agui` flow visible. +One Copilot agent handles normal conversation directly. Its instructions carry +only the heading list (table of contents) for two local READMEs; for a +documentation or code question it picks a heading and calls +`read_ag_ui_openai_agents_docs` or `read_ag_ui_protocol_docs` to read that one +section into context. The retrieval is deterministic and adds no vector +database, embedding request, network dependency, or extra agent hop — the +whole document never enters the prompt, and neither does a second model's +paraphrase of it. The endpoint keeps the direct `to_openai` → +`Runner.run_streamed` → `to_agui` flow visible. **Try:** `"Explain how to connect my existing OpenAI Agents SDK agent to AG-UI, -then ask the Documentation Specialist for the smallest FastAPI streaming -endpoint."` +then give me the smallest FastAPI streaming endpoint."` ### `agentic_chat` diff --git a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py index 7a700e59e4..1ea39f3c5c 100644 --- a/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py +++ b/integrations/openai-agents/python/examples/agents_examples/ag_ui_docs_copilot.py @@ -1,7 +1,8 @@ -"""AG-UI documentation assistant with two focused specialists as tools.""" +"""AG-UI documentation assistant that reads local README sections on demand.""" from __future__ import annotations +import re from importlib.metadata import metadata from agents import Agent, Runner, function_tool @@ -12,7 +13,8 @@ from ag_ui.encoder import EventEncoder from ag_ui_openai_agents import AGUITranslator from .constants import DEFAULT_MODEL -from .docs_search import MarkdownSearchIndex + +_HEADING_RE = re.compile(r"^#{1,6}\s+(.+)$") def _load_distribution_readme(distribution: str) -> str: @@ -23,124 +25,108 @@ def _load_distribution_readme(distribution: str) -> str: return document -# ag-ui-protocol specialist: knows the core Python SDK documentation. +class MarkdownSections: + """A Markdown document split by heading, read one section at a time. + + The Copilot sees only the heading list in its instructions and fetches + the sections it needs on demand, so the full document never sits in a + prompt. + """ + + def __init__(self, document: str) -> None: + sections: dict[str, list[str]] = {} + heading = "Overview" + for line in document.splitlines(): + match = _HEADING_RE.match(line) + if match: + heading = match.group(1).strip() + sections.setdefault(heading, []).append(line) + self._sections = { + name: content + for name, lines in sections.items() + if (content := "\n".join(lines).strip()) + } + + @property + def headings(self) -> str: + """The table of contents: one heading per line.""" + return "\n".join(f"- {name}" for name in self._sections) + + def read(self, heading: str) -> str: + """Return one section by heading (case-insensitive, substring ok).""" + wanted = heading.strip().lstrip("#").strip().lower() + for name, content in self._sections.items(): + if name.lower() == wanted: + return content + for name, content in self._sections.items(): + if wanted in name.lower(): + return content + return f"No section matches {heading!r}. Available headings:\n{self.headings}" + + +# ag-ui-protocol docs: the core Python SDK README. AG_UI_PROTOCOL_DOCS = _load_distribution_readme("ag-ui-protocol") -_AG_UI_PROTOCOL_DOCS_INDEX = MarkdownSearchIndex(AG_UI_PROTOCOL_DOCS) +_AG_UI_PROTOCOL_SECTIONS = MarkdownSections(AG_UI_PROTOCOL_DOCS) @function_tool -def search_ag_ui_protocol_docs(query: str) -> str: - """Find relevant AG-UI Protocol Python documentation sections. +def read_ag_ui_protocol_docs(heading: str) -> str: + """Read one AG-UI Protocol Python documentation section by its heading. Args: - query: The documentation question or API concepts to find. + heading: A heading from the "AG-UI Protocol docs" table of contents + in your instructions. """ - return _AG_UI_PROTOCOL_DOCS_INDEX.search(query) - + return _AG_UI_PROTOCOL_SECTIONS.read(heading) -AG_UI_PROTOCOL_DOCS_INSTRUCTIONS = """You are the technical specialist for -AG-UI Protocol's core Python SDK. Help developers understand ag_ui.core data -models, RunAgentInput, messages, events, event types, and EventEncoder. -Before answering, always call search_ag_ui_protocol_docs with a focused query. -Treat its excerpts as your only source of truth. If the first search is -insufficient, search once more with different API terms. - -Answer only the user's question. Retrieve and explain only the relevant parts -of the documentation; do not add a broad tutorial or unrelated integration -details. Be concise and practical. For code requests, provide only the -smallest complete, production-readable Python snippet needed for the request, -followed by a short explanation. Use documented APIs only. If the -documentation does not establish an answer, say so briefly instead of -guessing. -""" - -ag_ui_protocol_docs_agent = Agent( - name="AG-UI Protocol Python Specialist", - model=DEFAULT_MODEL, - instructions=AG_UI_PROTOCOL_DOCS_INSTRUCTIONS, - tools=[search_ag_ui_protocol_docs], -) - - -# ag-ui-openai-agents specialist: knows the integration documentation. +# ag-ui-openai-agents docs: this integration's README. AG_UI_OPENAI_AGENTS_DOCS = _load_distribution_readme("ag-ui-openai-agents") -_AG_UI_OPENAI_AGENTS_DOCS_INDEX = MarkdownSearchIndex(AG_UI_OPENAI_AGENTS_DOCS) +_AG_UI_OPENAI_AGENTS_SECTIONS = MarkdownSections(AG_UI_OPENAI_AGENTS_DOCS) @function_tool -def search_ag_ui_openai_agents_docs(query: str) -> str: - """Find relevant AG-UI OpenAI Agents integration documentation sections. +def read_ag_ui_openai_agents_docs(heading: str) -> str: + """Read one AG-UI OpenAI Agents integration documentation section. Args: - query: The documentation question or integration concepts to find. + heading: A heading from the "AG-UI OpenAI Agents docs" table of + contents in your instructions. """ - return _AG_UI_OPENAI_AGENTS_DOCS_INDEX.search(query) - - -AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS = """You are the technical specialist for -the AG-UI OpenAI Agents integration. Help developers understand the -AGUITranslator API, AG-UI request translation, SDK streaming, FastAPI/SSE -endpoints, tools, client tools, context, state, lifecycle events, errors, and -testing. - -Before answering, always call search_ag_ui_openai_agents_docs with a focused -query. Treat its excerpts as your only source of truth. If the first search is -insufficient, search once more with different API terms. - -Answer only the user's question. Retrieve and explain only the relevant parts -of the documentation; do not add a broad tutorial, related features, or extra -options unless the user asks. Be concise and practical. For code requests, -provide only the smallest complete, production-readable Python snippet needed -for the request, followed by a short explanation. Use documented APIs only. Do -not invent behavior or configuration. If the documentation does not establish -an answer, say so briefly instead of guessing. -""" + return _AG_UI_OPENAI_AGENTS_SECTIONS.read(heading) -ag_ui_openai_agents_docs_agent = Agent( - name="AG-UI OpenAI Agents Specialist", - model=DEFAULT_MODEL, - instructions=AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS, - tools=[search_ag_ui_openai_agents_docs], -) - -# Main Copilot: handles normal conversation and delegates documentation work. -copilot_instructions = """You are the developer-facing Copilot for an AG-UI +copilot_instructions = f"""You are the developer-facing Copilot for an AG-UI application. Answer only what the user asks. Be clear, practical, and concise; do not add a tutorial, unrelated details, alternatives, or follow-up work unless requested. Handle ordinary conversation directly. -For any question or request about AG-UI, the OpenAI Agents SDK, translator -APIs, FastAPI endpoints, streaming, tools, client tools, state, lifecycle -events, errors, tests, or Python implementation, call the relevant specialist -before answering. Use ask_ag_ui_openai_agents_docs for the OpenAI Agents SDK -integration and ask_ag_ui_protocol_docs for ag_ui.core protocol types and -EventEncoder questions. -Use only the part of the specialist's result that answers the user's question. -Do not invent behavior or claim details a specialist did not provide. For code -requests, make sure the relevant specialist is called.""" +For any question about AG-UI, the OpenAI Agents SDK, translator APIs, FastAPI +endpoints, streaming, tools, client tools, state, lifecycle events, errors, +tests, or Python implementation, pick the most relevant heading from the +matching table of contents below and call the matching tool before answering. +Use read_ag_ui_openai_agents_docs for OpenAI Agents SDK integration questions +and read_ag_ui_protocol_docs for ag_ui.core protocol types and EventEncoder +questions. Treat the returned section as your only source of truth. Read +another section if the first one is not enough. + +Use only the part of the section that answers the user's question. Do not +invent behavior or claim details the documentation did not provide. For code +requests, provide only the smallest complete, production-readable Python +snippet needed, followed by a short explanation. Use documented APIs only. + +AG-UI Protocol docs table of contents: +{_AG_UI_PROTOCOL_SECTIONS.headings} + +AG-UI OpenAI Agents docs table of contents: +{_AG_UI_OPENAI_AGENTS_SECTIONS.headings} +""" copilot_agent = Agent( name="AG-UI Docs Copilot", model=DEFAULT_MODEL, instructions=copilot_instructions, - tools=[ - ag_ui_protocol_docs_agent.as_tool( - tool_name="ask_ag_ui_protocol_docs", - tool_description=( - "Provide authoritative AG-UI core Python SDK guidance about " - "protocol types, RunAgentInput, events, and EventEncoder." - ), - ), - ag_ui_openai_agents_docs_agent.as_tool( - tool_name="ask_ag_ui_openai_agents_docs", - tool_description=( - "Provide authoritative AG-UI and OpenAI Agents SDK guidance, " - "including documented Python integration snippets." - ), - ), - ], + tools=[read_ag_ui_protocol_docs, read_ag_ui_openai_agents_docs], ) # AGUI Translator Integration diff --git a/integrations/openai-agents/python/examples/agents_examples/docs_search.py b/integrations/openai-agents/python/examples/agents_examples/docs_search.py deleted file mode 100644 index 4f06dd5b38..0000000000 --- a/integrations/openai-agents/python/examples/agents_examples/docs_search.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Small, dependency-free Markdown retrieval for the docs Copilot example.""" - -from __future__ import annotations - -import re -from collections import Counter -from dataclasses import dataclass -from math import log - - -_WORD_RE = re.compile(r"[a-z0-9]+") -_HEADING_RE = re.compile(r"^(#{1,6})\s+(.+)$") -_STOP_WORDS = { - "a", - "an", - "about", - "and", - "are", - "do", - "for", - "from", - "how", - "i", - "in", - "is", - "it", - "of", - "on", - "the", - "to", - "use", - "what", - "with", -} - - -def _terms(value: str) -> list[str]: - """Normalize prose and Python identifiers into searchable terms.""" - value = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", value).replace("_", "-") - return [term for term in _WORD_RE.findall(value.lower()) if term not in _STOP_WORDS] - - -@dataclass(frozen=True) -class _Section: - heading: str - content: str - heading_terms: frozenset[str] - content_terms: tuple[str, ...] - - -class MarkdownSearchIndex: - """Rank Markdown sections with BM25 and return the relevant excerpts.""" - - def __init__(self, document: str) -> None: - self._sections = self._split_sections(document) - self._document_frequency = Counter( - term for section in self._sections for term in set(section.content_terms) - ) - self._average_section_length = ( - sum(len(section.content_terms) for section in self._sections) - / len(self._sections) - if self._sections - else 0.0 - ) - - @staticmethod - def _split_sections(document: str) -> list[_Section]: - sections: list[_Section] = [] - heading = "Overview" - lines: list[str] = [] - - def append_section() -> None: - content = "\n".join(lines).strip() - if not content: - return - sections.append( - _Section( - heading=heading, - content=content, - heading_terms=frozenset(_terms(heading)), - content_terms=tuple(_terms(content)), - ) - ) - - for line in document.splitlines(): - match = _HEADING_RE.match(line) - if match: - append_section() - heading = match.group(2).strip() - lines = [line] - else: - lines.append(line) - append_section() - return sections - - def search(self, query: str, *, limit: int = 3, max_chars: int = 7_000) -> str: - """Return the highest-scoring sections within a character budget.""" - query_terms = set(_terms(query)) - if not query_terms: - return "No specific documentation terms were supplied." - - ranked: list[tuple[float, int, _Section]] = [] - for position, section in enumerate(self._sections): - frequencies = Counter(section.content_terms) - score = 0.0 - for term in query_terms: - frequency = frequencies[term] - if not frequency: - continue - document_frequency = self._document_frequency[term] - inverse_frequency = log( - 1 - + (len(self._sections) - document_frequency + 0.5) - / (document_frequency + 0.5) - ) - length_ratio = len(section.content_terms) / self._average_section_length - saturation = (frequency * 2.2) / ( - frequency + 1.2 * (0.25 + 0.75 * length_ratio) - ) - score += inverse_frequency * saturation - if term in section.heading_terms: - score += inverse_frequency * 2 - if score: - ranked.append((score, -position, section)) - - if not ranked: - return "No relevant section was found in this documentation source." - - excerpts: list[str] = [] - used_chars = 0 - for _, _, section in sorted(ranked, reverse=True)[:limit]: - separator_size = 2 if excerpts else 0 - if used_chars + separator_size + len(section.content) > max_chars: - continue - excerpts.append(section.content) - used_chars += separator_size + len(section.content) - - if not excerpts: - return "The closest documentation section exceeds the retrieval budget." - return "\n\n".join(excerpts) - - -__all__ = ["MarkdownSearchIndex"] diff --git a/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py b/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py index 64969dcbd1..9350bed238 100644 --- a/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py +++ b/integrations/openai-agents/python/examples/tests/test_ag_ui_docs_copilot.py @@ -10,7 +10,7 @@ sys.path.insert(0, str(EXAMPLES)) from agents_examples import ag_ui_docs_copilot # noqa: E402 -from agents_examples.docs_search import MarkdownSearchIndex # noqa: E402 +from agents_examples.ag_ui_docs_copilot import MarkdownSections # noqa: E402 def test_docs_copilot_loads_the_integration_readme() -> None: @@ -20,55 +20,46 @@ def test_docs_copilot_loads_the_integration_readme() -> None: assert "EventEncoder" in ag_ui_docs_copilot.AG_UI_PROTOCOL_DOCS -def test_docs_copilot_has_a_documentation_specialist_tool() -> None: +def test_docs_copilot_reads_both_doc_sources_directly() -> None: assert ag_ui_docs_copilot.copilot_agent.name == "AG-UI Docs Copilot" assert {tool.name for tool in ag_ui_docs_copilot.copilot_agent.tools} == { - "ask_ag_ui_openai_agents_docs", - "ask_ag_ui_protocol_docs", - } - assert ( - ag_ui_docs_copilot.ag_ui_openai_agents_docs_agent.name - == "AG-UI OpenAI Agents Specialist" - ) - assert ( - ag_ui_docs_copilot.ag_ui_protocol_docs_agent.name - == "AG-UI Protocol Python Specialist" - ) - assert { - tool.name for tool in ag_ui_docs_copilot.ag_ui_openai_agents_docs_agent.tools - } == {"search_ag_ui_openai_agents_docs"} - assert {tool.name for tool in ag_ui_docs_copilot.ag_ui_protocol_docs_agent.tools} == { - "search_ag_ui_protocol_docs" + "read_ag_ui_openai_agents_docs", + "read_ag_ui_protocol_docs", } -def test_docs_copilot_retrieves_only_relevant_readme_sections() -> None: - excerpts = ag_ui_docs_copilot._AG_UI_OPENAI_AGENTS_DOCS_INDEX.search( - "How do I stream AGUITranslator events from a FastAPI endpoint?" - ) +def test_docs_copilot_instructions_carry_both_tocs_but_not_the_docs() -> None: + instructions = ag_ui_docs_copilot.copilot_instructions + assert ag_ui_docs_copilot._AG_UI_PROTOCOL_SECTIONS.headings in instructions + assert ag_ui_docs_copilot._AG_UI_OPENAI_AGENTS_SECTIONS.headings in instructions + assert ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS not in instructions + assert ag_ui_docs_copilot.AG_UI_PROTOCOL_DOCS not in instructions - assert "Runner.run_streamed" in excerpts - assert len(excerpts) < len(ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS) - assert ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS not in ( - ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS_INSTRUCTIONS - ) - assert ag_ui_docs_copilot.AG_UI_PROTOCOL_DOCS not in ( - ag_ui_docs_copilot.AG_UI_PROTOCOL_DOCS_INSTRUCTIONS + +def test_docs_copilot_reads_one_section_by_heading() -> None: + section = ag_ui_docs_copilot._AG_UI_OPENAI_AGENTS_SECTIONS.read( + "Backend tool approval" ) + assert "needs_approval" in section + assert len(section) < len(ag_ui_docs_copilot.AG_UI_OPENAI_AGENTS_DOCS) + - approval_excerpts = ag_ui_docs_copilot._AG_UI_OPENAI_AGENTS_DOCS_INDEX.search( - "How do I resume an interrupted run after approval?" +def test_markdown_sections_splits_and_matches_loosely() -> None: + sections = MarkdownSections( + "intro line\n\n## Setup\npip install it\n\n## Usage Notes\ncall run()\n" ) - assert "## Backend tool approval (`needs_approval`)" in approval_excerpts + assert sections.headings == "- Overview\n- Setup\n- Usage Notes" + assert sections.read("Setup") == "## Setup\npip install it" + assert sections.read("## usage notes") == "## Usage Notes\ncall run()" + assert sections.read("usage") == "## Usage Notes\ncall run()" + assert "Available headings" in sections.read("missing") -def test_markdown_search_handles_empty_documents() -> None: +def test_markdown_sections_handles_empty_documents() -> None: for document in ("", " \n\t"): - index = MarkdownSearchIndex(document) - assert ( - index.search("AGUITranslator") - == "No relevant section was found in this documentation source." - ) + sections = MarkdownSections(document) + assert sections.headings == "" + assert "Available headings" in sections.read("AGUITranslator") def test_docs_copilot_keeps_the_direct_translator_flow_visible() -> None: From 3f37103fbfb12c8e3bbafeb4ce03b53f2e48eb9f Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sat, 18 Jul 2026 13:25:34 +0200 Subject: [PATCH 79/94] fix(openai-agents): remove duplicate import --- .../python/tests/engine/test_agui_to_openai_multimodal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py b/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py index f6766a3c67..e68dac38cf 100644 --- a/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py +++ b/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py @@ -11,6 +11,8 @@ import warnings import pytest +from pydantic import ValidationError + from ag_ui.core import ( AudioInputContent, BinaryInputContent, @@ -23,8 +25,6 @@ UserMessage, VideoInputContent, ) -from pydantic import ValidationError - from ag_ui_openai_agents import AGUITranslator from ag_ui_openai_agents.engine.agui_to_openai import AGUIToOpenAITranslator from ag_ui_openai_agents.engine.types import TranslatedInput From e659f6aa699d4e5f13507c25885574f6ecd48380 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 01:49:23 +0200 Subject: [PATCH 80/94] fix(openai-agents): pass CustomEvent instances to custom_lifecycle_events, not factories returning them --- .../(v2)/custom_lifecycle_events/page.tsx | 8 ++++--- apps/dojo/src/files.json | 4 ++-- .../custom_lifecycle_events.py | 24 ++++++++----------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx index 7d16223fdf..58a84afecd 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx @@ -59,7 +59,6 @@ function UsageLabel({ () => getRunUsage(runId), () => undefined, ); - if ( message.role !== "assistant" || position !== "after" || @@ -128,8 +127,11 @@ const Chat = () => { runUsage.delete(event.runId); usageListeners.forEach((listener) => listener()); }, - onCustomEvent: ({ event }) => { - const runId = currentRunId.current; + onCustomEvent: ({ event, input }) => { + // The subscriber context carries the run input for every event. Use + // it directly so usage events are not lost if RUN_STARTED and CUSTOM + // are delivered before the lifecycle callback updates the ref. + const runId = input.runId ?? currentRunId.current; if (!runId) return; const value = event.value as { tokens: number; cost_usd: number }; diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index dd2f788235..0a5f16be74 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4968,7 +4968,7 @@ "openai-agents-python::custom_lifecycle_events": [ { "name": "page.tsx", - "content": "\"use client\";\n\nimport React, {\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface UsageInfo {\n tokens: number;\n costUsd: number;\n}\n\ninterface RunUsage {\n input?: UsageInfo;\n output?: UsageInfo;\n}\n\nconst runUsage = new Map();\nconst usageListeners = new Set<() => void>();\n\nfunction setRunUsage(runId: string, update: RunUsage) {\n runUsage.set(runId, { ...runUsage.get(runId), ...update });\n usageListeners.forEach((listener) => listener());\n}\n\nfunction subscribeRunUsage(listener: () => void) {\n usageListeners.add(listener);\n return () => usageListeners.delete(listener);\n}\n\nfunction getRunUsage(runId?: string) {\n return runId ? runUsage.get(runId) : undefined;\n}\n\nfunction UsageLabel({\n message,\n position,\n runId,\n}: {\n message: { role: string };\n position: \"before\" | \"after\";\n runId?: string;\n}) {\n const usage = useSyncExternalStore(\n subscribeRunUsage,\n () => getRunUsage(runId),\n () => undefined,\n );\n\n if (\n message.role !== \"assistant\" ||\n position !== \"after\" ||\n (!usage?.input && !usage?.output)\n ) {\n return null;\n }\n\n return (\n \n {usage.input && (\n \n ⬇️ {usage.input.tokens} tokens · ${usage.input.costUsd.toFixed(6)}\n \n )}\n {usage.input && usage.output && }\n {usage.output && (\n \n ⬆️ {usage.output.tokens} tokens · ${usage.output.costUsd.toFixed(6)}\n \n )}\n \n );\n}\n\nconst CustomLifecycleEvents: React.FC = ({\n params,\n}) => {\n const { integrationId } = React.use(params);\n const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const currentRunId = useRef(undefined);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n {\n title: \"Tell me a fact about AI\",\n message: \"Tell me one interesting fact about AI.\",\n },\n ],\n available: \"before-first-message\",\n consumerAgentId: \"custom_lifecycle_events\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onRunStartedEvent: ({ event }) => {\n currentRunId.current = event.runId;\n runUsage.delete(event.runId);\n usageListeners.forEach((listener) => listener());\n },\n onCustomEvent: ({ event }) => {\n const runId = currentRunId.current;\n if (!runId) return;\n\n const value = event.value as { tokens: number; cost_usd: number };\n if (event.name === \"input_usage\") {\n setRunUsage(runId, {\n input: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n if (event.name === \"output_usage\") {\n setRunUsage(runId, {\n output: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n },\n onRunFinishedEvent: () => {\n currentRunId.current = undefined;\n },\n onRunErrorEvent: () => {\n currentRunId.current = undefined;\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default CustomLifecycleEvents;\n", + "content": "\"use client\";\n\nimport React, {\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface UsageInfo {\n tokens: number;\n costUsd: number;\n}\n\ninterface RunUsage {\n input?: UsageInfo;\n output?: UsageInfo;\n}\n\nconst runUsage = new Map();\nconst usageListeners = new Set<() => void>();\n\nfunction setRunUsage(runId: string, update: RunUsage) {\n runUsage.set(runId, { ...runUsage.get(runId), ...update });\n usageListeners.forEach((listener) => listener());\n}\n\nfunction subscribeRunUsage(listener: () => void) {\n usageListeners.add(listener);\n return () => usageListeners.delete(listener);\n}\n\nfunction getRunUsage(runId?: string) {\n return runId ? runUsage.get(runId) : undefined;\n}\n\nfunction UsageLabel({\n message,\n position,\n runId,\n}: {\n message: { role: string };\n position: \"before\" | \"after\";\n runId?: string;\n}) {\n const usage = useSyncExternalStore(\n subscribeRunUsage,\n () => getRunUsage(runId),\n () => undefined,\n );\n\n if (\n message.role !== \"assistant\" ||\n position !== \"after\" ||\n (!usage?.input && !usage?.output)\n ) {\n return null;\n }\n\n return (\n \n {usage.input && (\n \n ⬇️ {usage.input.tokens} tokens · ${usage.input.costUsd.toFixed(6)}\n \n )}\n {usage.input && usage.output && }\n {usage.output && (\n \n ⬆️ {usage.output.tokens} tokens · ${usage.output.costUsd.toFixed(6)}\n \n )}\n \n );\n}\n\nconst CustomLifecycleEvents: React.FC = ({\n params,\n}) => {\n const { integrationId } = React.use(params);\n const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const currentRunId = useRef(undefined);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n {\n title: \"Tell me a fact about AI\",\n message: \"Tell me one interesting fact about AI.\",\n },\n ],\n available: \"before-first-message\",\n consumerAgentId: \"custom_lifecycle_events\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onRunStartedEvent: ({ event }) => {\n currentRunId.current = event.runId;\n runUsage.delete(event.runId);\n usageListeners.forEach((listener) => listener());\n },\n onCustomEvent: ({ event, input }) => {\n // The subscriber context carries the run input for every event. Use\n // it directly so usage events are not lost if RUN_STARTED and CUSTOM\n // are delivered before the lifecycle callback updates the ref.\n const runId = input.runId ?? currentRunId.current;\n if (!runId) return;\n\n const value = event.value as { tokens: number; cost_usd: number };\n if (event.name === \"input_usage\") {\n setRunUsage(runId, {\n input: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n if (event.name === \"output_usage\") {\n setRunUsage(runId, {\n output: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n },\n onRunFinishedEvent: () => {\n currentRunId.current = undefined;\n },\n onRunErrorEvent: () => {\n currentRunId.current = undefined;\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default CustomLifecycleEvents;\n", "language": "typescript", "type": "file" }, @@ -4980,7 +4980,7 @@ }, { "name": "custom_lifecycle_events.py", - "content": "\"\"\"Custom lifecycle events — CUSTOM events bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. The wrapper receives them as\n``start_custom_event``/``end_custom_event`` factories and forwards their\nresults to the same-named ``to_agui()`` parameters\nparams, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_event() -> CustomEvent:\n tokens = random.randint(20, 120)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"input_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)},\n )\n\n\ndef build_output_usage_event() -> CustomEvent:\n tokens = random.randint(120, 480)\n return CustomEvent(\n type=EventType.CUSTOM,\n name=\"output_usage\",\n value={\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)},\n )\n\n\nagent = OpenAIAgentsAgent(\n create_custom_lifecycle_events_agent(),\n name=\"custom_lifecycle_events\",\n start_custom_event=build_input_usage_event,\n end_custom_event=build_output_usage_event,\n)\napp = FastAPI(title=\"Custom lifecycle events AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "content": "\"\"\"Custom lifecycle events — CUSTOM events bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. The wrapper receives them as\n``start_custom_event``/``end_custom_event`` factories and forwards their\nresults to the same-named ``to_agui()`` parameters\nparams, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_value() -> dict[str, float]:\n tokens = random.randint(20, 120)\n return {\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)}\n\n\ndef build_output_usage_value() -> dict[str, float]:\n tokens = random.randint(120, 480)\n return {\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)}\n\n\nagent = OpenAIAgentsAgent(\n create_custom_lifecycle_events_agent(),\n name=\"custom_lifecycle_events\",\n start_custom_event=CustomEvent(\n type=EventType.CUSTOM, name=\"input_usage\", value=build_input_usage_value\n ),\n end_custom_event=CustomEvent(\n type=EventType.CUSTOM, name=\"output_usage\", value=build_output_usage_value\n ),\n)\napp = FastAPI(title=\"Custom lifecycle events AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } diff --git a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py index b520399022..be90d5d216 100644 --- a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py +++ b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py @@ -35,29 +35,25 @@ def create_custom_lifecycle_events_agent() -> Agent: ) -def build_input_usage_event() -> CustomEvent: +def build_input_usage_value() -> dict[str, float]: tokens = random.randint(20, 120) - return CustomEvent( - type=EventType.CUSTOM, - name="input_usage", - value={"tokens": tokens, "cost_usd": round(tokens * 0.00000015, 6)}, - ) + return {"tokens": tokens, "cost_usd": round(tokens * 0.00000015, 6)} -def build_output_usage_event() -> CustomEvent: +def build_output_usage_value() -> dict[str, float]: tokens = random.randint(120, 480) - return CustomEvent( - type=EventType.CUSTOM, - name="output_usage", - value={"tokens": tokens, "cost_usd": round(tokens * 0.0000006, 6)}, - ) + return {"tokens": tokens, "cost_usd": round(tokens * 0.0000006, 6)} agent = OpenAIAgentsAgent( create_custom_lifecycle_events_agent(), name="custom_lifecycle_events", - start_custom_event=build_input_usage_event, - end_custom_event=build_output_usage_event, + start_custom_event=CustomEvent( + type=EventType.CUSTOM, name="input_usage", value=build_input_usage_value + ), + end_custom_event=CustomEvent( + type=EventType.CUSTOM, name="output_usage", value=build_output_usage_value + ), ) app = FastAPI(title="Custom lifecycle events AG-UI demo") add_openai_agents_fastapi_endpoint(app, agent, "/") From cb48d1c5989d37fb4a9b0fa28ce5c4f47ded8a39 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 02:11:29 +0200 Subject: [PATCH 81/94] feat(openai-agents): report real input/output token counts in custom_lifecycle_events --- .../(v2)/custom_lifecycle_events/README.mdx | 50 +++++---- .../(v2)/custom_lifecycle_events/page.tsx | 104 ++++++------------ .../custom_lifecycle_events.py | 97 ++++++++-------- 3 files changed, 114 insertions(+), 137 deletions(-) diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx index 47c0841b52..8a3a2af9d2 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx @@ -2,36 +2,42 @@ ## What This Demo Shows -`AGUITranslator.to_agui()` takes two optional params — `start_custom_event` -and `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance -(anything else raises `TypeError`), default `None` (off). When set, one -`CUSTOM` event is emitted right after `RUN_STARTED`, another right before -`RUN_FINISHED`. This demo's `OpenAIAgentsAgent` wrapper forwards them to -report fake usage split the way a real bill would be: `input_usage` -(prompt tokens/cost — known before the model even runs) at the start, -`output_usage` (completion tokens/cost — known only once the run is done) -at the end — plain conversation otherwise, same agent as `agentic_chat`. +`AGUITranslator.to_agui()` takes an optional `end_custom_event` param — +an AG-UI `CustomEvent` instance (anything else raises `TypeError`), default +`None` (off). When set, one `CUSTOM` event is emitted right before +`RUN_FINISHED`. This demo uses it to report the run's real token usage: +`run_usage`, with `input_tokens`/`output_tokens` read off the SDK's own +`RunResultStreaming.context_wrapper.usage` after the stream finishes — plain +conversation otherwise, same agent as `agentic_chat`. + +Real usage is only known once the run is done — there's no such thing as a +true token count before the model has answered — so this only fires once, +at the end, not at both `RUN_STARTED` and `RUN_FINISHED` like a +demonstration bracketing pair would. ## How to Interact - "Say hi in one sentence." - "Tell me one interesting fact about AI." -Each assistant reply has a compact usage label immediately above it. The label -shows the input usage reported at the beginning of the run and the output usage -reported at the end. +Each assistant reply has a compact usage label immediately above it, showing +the real input and output token counts for that run. ## Technical Details -- `build_input_usage_event()`/`build_output_usage_event()` - (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s - for each run; `OpenAIAgentsAgent` calls the builders and passes the results - to `to_agui(start_custom_event=..., end_custom_event=...)` -- Both events are opaque to the translator — it only checks `isinstance(..., - CustomEvent)`, the `name`/`value` shape is entirely up to the caller +- `agents_examples/custom_lifecycle_events.py` composes the translator + directly (`translator.to_openai`/`translator.to_agui`) instead of the + `OpenAIAgentsAgent` wrapper, because reading the run's real usage requires + the `RunResultStreaming` object the wrapper doesn't expose to a lifecycle + event factory +- `end_custom_event`'s value is a zero-argument closure over that `result`, + resolved by the translator right before it emits the event — by then the + stream is fully drained and `context_wrapper.usage` holds the run's actual + totals +- The translator itself is opaque to the event's contents — it only checks + `isinstance(..., CustomEvent)`; the `name`/`value` shape is entirely up to + the caller - The page captures the current `runId` from `RUN_STARTED`, associates the - custom usage events with that run in a small external store, then uses + `run_usage` event with that run in a small external store, then uses `CopilotKitProvider`'s stable `renderCustomMessages` configuration to render - the matching label above the assistant message -- The token/cost numbers are fake — the point is the event bracketing, not - real usage accounting + the label above the assistant message diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx index 58a84afecd..fdf79c178a 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx @@ -1,11 +1,6 @@ "use client"; -import React, { - useEffect, - useMemo, - useRef, - useSyncExternalStore, -} from "react"; +import React, { useEffect, useMemo, useSyncExternalStore } from "react"; import "@copilotkit/react-core/v2/styles.css"; import { CopilotChat, @@ -18,52 +13,45 @@ interface CustomLifecycleEventsProps { params: Promise<{ integrationId: string }>; } -interface UsageInfo { - tokens: number; - costUsd: number; -} - interface RunUsage { - input?: UsageInfo; - output?: UsageInfo; + inputTokens: number; + outputTokens: number; } -const runUsage = new Map(); +// Keyed by the assistant message's own id rather than the framework's +// run id: CopilotKit's message-to-run mapping only resolves reliably for +// the first run in a session, so a run-id-keyed store silently drops usage +// on every later turn. Message ids don't have that problem. +const usageByMessageId = new Map(); const usageListeners = new Set<() => void>(); -function setRunUsage(runId: string, update: RunUsage) { - runUsage.set(runId, { ...runUsage.get(runId), ...update }); +function setMessageUsage(messageId: string, usage: RunUsage) { + usageByMessageId.set(messageId, usage); usageListeners.forEach((listener) => listener()); } -function subscribeRunUsage(listener: () => void) { +function subscribeUsage(listener: () => void) { usageListeners.add(listener); return () => usageListeners.delete(listener); } -function getRunUsage(runId?: string) { - return runId ? runUsage.get(runId) : undefined; +function getMessageUsage(messageId: string) { + return usageByMessageId.get(messageId); } function UsageLabel({ message, position, - runId, }: { - message: { role: string }; + message: { id: string; role: string }; position: "before" | "after"; - runId?: string; }) { const usage = useSyncExternalStore( - subscribeRunUsage, - () => getRunUsage(runId), + subscribeUsage, + () => getMessageUsage(message.id), () => undefined, ); - if ( - message.role !== "assistant" || - position !== "after" || - (!usage?.input && !usage?.output) - ) { + if (message.role !== "assistant" || position !== "after" || !usage) { return null; } @@ -72,17 +60,8 @@ function UsageLabel({ className="mb-2 px-1 text-xs text-muted-foreground" aria-label="Run usage" > - {usage.input && ( - - ⬇️ {usage.input.tokens} tokens · ${usage.input.costUsd.toFixed(6)} - - )} - {usage.input && usage.output && } - {usage.output && ( - - ⬆️ {usage.output.tokens} tokens · ${usage.output.costUsd.toFixed(6)} - - )} + ⬇️ {usage.inputTokens} input tokens · ⬆️ {usage.outputTokens} output + tokens ); } @@ -106,7 +85,6 @@ const CustomLifecycleEvents: React.FC = ({ const Chat = () => { const { agent } = useAgent({ agentId: "custom_lifecycle_events" }); - const currentRunId = useRef(undefined); useConfigureSuggestions({ suggestions: [ @@ -122,35 +100,21 @@ const Chat = () => { useEffect(() => { const subscription = agent.subscribe({ - onRunStartedEvent: ({ event }) => { - currentRunId.current = event.runId; - runUsage.delete(event.runId); - usageListeners.forEach((listener) => listener()); - }, - onCustomEvent: ({ event, input }) => { - // The subscriber context carries the run input for every event. Use - // it directly so usage events are not lost if RUN_STARTED and CUSTOM - // are delivered before the lifecycle callback updates the ref. - const runId = input.runId ?? currentRunId.current; - if (!runId) return; - - const value = event.value as { tokens: number; cost_usd: number }; - if (event.name === "input_usage") { - setRunUsage(runId, { - input: { tokens: value.tokens, costUsd: value.cost_usd }, - }); - } - if (event.name === "output_usage") { - setRunUsage(runId, { - output: { tokens: value.tokens, costUsd: value.cost_usd }, - }); - } - }, - onRunFinishedEvent: () => { - currentRunId.current = undefined; - }, - onRunErrorEvent: () => { - currentRunId.current = undefined; + onCustomEvent: ({ event, messages }) => { + if (event.name !== "run_usage") return; + // run_usage fires after MESSAGES_SNAPSHOT, so the assistant's + // reply is already the last message in this run's history. + const lastMessage = messages[messages.length - 1]; + if (!lastMessage) return; + + const value = event.value as { + input_tokens: number; + output_tokens: number; + }; + setMessageUsage(lastMessage.id, { + inputTokens: value.input_tokens, + outputTokens: value.output_tokens, + }); }, }); return () => subscription.unsubscribe(); diff --git a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py index be90d5d216..f96994698e 100644 --- a/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py +++ b/integrations/openai-agents/python/examples/agents_examples/custom_lifecycle_events.py @@ -1,59 +1,66 @@ -"""Custom lifecycle events — CUSTOM events bracketing the run. - -Plain chat agent, same as agentic_chat — the point isn't the agent, it's -the two builder functions below. The wrapper receives them as -``start_custom_event``/``end_custom_event`` factories and forwards their -results to the same-named ``to_agui()`` parameters -params, so one -CUSTOM event goes out right after RUN_STARTED and another right before -RUN_FINISHED. Only CustomEvent instances are accepted there; anything else -raises TypeError. - -input_usage fires at the start because prompt tokens/cost are known before -the model even runs; output_usage fires at the end because completion -tokens/cost are only known once the run is done. Numbers here are fake — -the point is the event bracketing, not real usage accounting. +"""Custom lifecycle events — a real usage summary right before RUN_FINISHED. + +Plain chat agent, same as agentic_chat — the point isn't the agent, it's the +CUSTOM event at the end. The SDK only knows real input/output token counts +once the run has finished, so this demo composes the translator directly +(rather than the OpenAIAgentsAgent wrapper) to read them off the run result +after the stream completes and before to_agui() emits RUN_FINISHED. """ from __future__ import annotations -import random - -from agents import Agent -from fastapi import FastAPI +from agents import Agent, Runner +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse -from ag_ui.core import CustomEvent, EventType -from ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint +from ag_ui.core import CustomEvent, EventType, RunAgentInput +from ag_ui.encoder import EventEncoder +from ag_ui_openai_agents import AGUITranslator from .constants import DEFAULT_MODEL +agent = Agent( + name="assistant", + model=DEFAULT_MODEL, + instructions="You are a helpful assistant. Be concise.", +) -def create_custom_lifecycle_events_agent() -> Agent: - return Agent( - name="assistant", - model=DEFAULT_MODEL, - instructions="You are a helpful assistant. Be concise.", - ) +app = FastAPI(title="Custom lifecycle events AG-UI demo") +translator = AGUITranslator() -def build_input_usage_value() -> dict[str, float]: - tokens = random.randint(20, 120) - return {"tokens": tokens, "cost_usd": round(tokens * 0.00000015, 6)} +@app.post("/") +async def run_custom_lifecycle_events( + body: RunAgentInput, request: Request +) -> StreamingResponse: + """Translate one AG-UI request into an SDK run, then report real token usage.""" + encoder = EventEncoder(accept=request.headers.get("accept")) + async def stream(): + # AGUI input -> OpenAI SDK + translated_input = translator.to_openai(body) + result = Runner.run_streamed( + agent, + input=translated_input.messages, + context=translated_input.context, + ) -def build_output_usage_value() -> dict[str, float]: - tokens = random.randint(120, 480) - return {"tokens": tokens, "cost_usd": round(tokens * 0.0000006, 6)} + def usage_value() -> dict[str, int]: + # Resolved after the stream finishes, so these are the run's + # actual totals, not an estimate made up before anything ran. + usage = result.context_wrapper.usage + return { + "input_tokens": usage.input_tokens, + "output_tokens": usage.output_tokens, + } + end_custom_event = CustomEvent( + type=EventType.CUSTOM, name="run_usage", value=usage_value + ) -agent = OpenAIAgentsAgent( - create_custom_lifecycle_events_agent(), - name="custom_lifecycle_events", - start_custom_event=CustomEvent( - type=EventType.CUSTOM, name="input_usage", value=build_input_usage_value - ), - end_custom_event=CustomEvent( - type=EventType.CUSTOM, name="output_usage", value=build_output_usage_value - ), -) -app = FastAPI(title="Custom lifecycle events AG-UI demo") -add_openai_agents_fastapi_endpoint(app, agent, "/") + # OpenAI SDK -> AGUI events + async for event in translator.to_agui( + result, body, end_custom_event=end_custom_event + ): + yield encoder.encode(event) + + return StreamingResponse(stream(), media_type=encoder.get_content_type()) From 9d22b98c07c7e77cedc23eb4e8cebb75973e2e88 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 02:25:13 +0200 Subject: [PATCH 82/94] refactor(examples): drop tracing override, defer to SDK's own OPENAI_AGENTS_DISABLE_TRACING default --- integrations/openai-agents/python/examples/.env.example | 4 ++++ integrations/openai-agents/python/examples/README.md | 4 +++- integrations/openai-agents/python/examples/server.py | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/integrations/openai-agents/python/examples/.env.example b/integrations/openai-agents/python/examples/.env.example index 2b26eefcf1..e26b21383b 100644 --- a/integrations/openai-agents/python/examples/.env.example +++ b/integrations/openai-agents/python/examples/.env.example @@ -3,3 +3,7 @@ OPENAI_API_KEY=sk-your-key-here # Optional: override the model used by every example agent # OPENAI_DEFAULT_MODEL=gpt-4.1-mini + +# Optional: OPENAI_AGENTS_DISABLE_TRACING is the OpenAI Agents SDK's own env +# var for tracing. SDK default is "false" (tracing on). Set to "true" to disable tracing. +# OPENAI_AGENTS_DISABLE_TRACING=false diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 7d731794ce..85c6bf85d5 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -24,7 +24,9 @@ uv run --no-dev python server.py ``` Server runs on **http://localhost:8024** (the port the AG-UI Dojo expects; -override with `PORT`). +override with `PORT`). Tracing follows the SDK's own `OPENAI_AGENTS_DISABLE_TRACING` +env var and default (`false` — tracing on); set it to `true` to disable +tracing. ## Automated tests diff --git a/integrations/openai-agents/python/examples/server.py b/integrations/openai-agents/python/examples/server.py index 84e458f21e..f485815b42 100644 --- a/integrations/openai-agents/python/examples/server.py +++ b/integrations/openai-agents/python/examples/server.py @@ -10,7 +10,6 @@ from pathlib import Path import uvicorn -from agents import set_tracing_disabled from fastapi import FastAPI from agents_examples import ( @@ -25,7 +24,8 @@ tool_based_generative_ui, ) -set_tracing_disabled(True) +# Tracing follows the SDK's own OPENAI_AGENTS_DISABLE_TRACING env var and its +# own default (tracing on) — nothing to set up here. DEMOS = { "ag_ui_docs_copilot": ag_ui_docs_copilot.copilot_agent, From 34d269a869264fc5e6cde0cd6f4d4b3c797146a3 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 03:23:14 +0200 Subject: [PATCH 83/94] docs(openai-agents): add LICENSE and license metadata --- integrations/openai-agents/python/LICENSE | 21 +++++++++++++++++++ .../openai-agents/python/pyproject.toml | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 integrations/openai-agents/python/LICENSE diff --git a/integrations/openai-agents/python/LICENSE b/integrations/openai-agents/python/LICENSE new file mode 100644 index 0000000000..b77bf2ab72 --- /dev/null +++ b/integrations/openai-agents/python/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml index 3d0a164e04..af77bd1487 100644 --- a/integrations/openai-agents/python/pyproject.toml +++ b/integrations/openai-agents/python/pyproject.toml @@ -2,6 +2,8 @@ name = "ag-ui-openai-agents" version = "0.1.0" description = "AG-UI integration for OpenAI Agent SDK" +license = "MIT" +license-files = ["LICENSE"] readme = "README.md" authors = [ { name = "Abdelrahman Abozied", email = "me@abdelrahman.ai" }, From 36164f0623f7f8c5ebe0d25854d1988787d061f7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 05:12:19 +0200 Subject: [PATCH 84/94] feat(pyproject): add test script configuration for pytest --- integrations/openai-agents/python/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml index af77bd1487..4aa1733311 100644 --- a/integrations/openai-agents/python/pyproject.toml +++ b/integrations/openai-agents/python/pyproject.toml @@ -25,6 +25,9 @@ dev = [ [tool.pytest.ini_options] testpaths = ["tests"] +[tool.ag-ui.scripts] +test = "python -m pytest" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" From 1cd5b6ef50c3f8b2626a2f9673a03dbdfb7ae7f3 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 05:14:46 +0200 Subject: [PATCH 85/94] ci(openai-agents): add dojo e2e matrix entry and agentic_chat spec --- .github/workflows/dojo-e2e.yml | 4 ++ .../agenticChatPage.spec.ts | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 apps/dojo/e2e/tests/openaiAgentsPythonTests/agenticChatPage.spec.ts diff --git a/.github/workflows/dojo-e2e.yml b/.github/workflows/dojo-e2e.yml index ea031b1fe6..f7fcd07b27 100644 --- a/.github/workflows/dojo-e2e.yml +++ b/.github/workflows/dojo-e2e.yml @@ -174,6 +174,10 @@ jobs: test_path: tests/agUiDotnetTests services: ["dojo", "ag-ui-dotnet"] wait_on: http://localhost:9999,tcp:localhost:8023 + - suite: openai-agents-python + test_path: tests/openaiAgentsPythonTests + services: ["dojo", "openai-agents-python"] + wait_on: http://localhost:9999,tcp:localhost:8024 # - suite: vercel-ai-sdk # test_path: tests/vercelAISdkTests # services: ["dojo"] diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/agenticChatPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/agenticChatPage.spec.ts new file mode 100644 index 0000000000..458909c570 --- /dev/null +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/agenticChatPage.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from "../../test-isolation-helper"; +import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; + +test("[OpenAI Agents Python] Agentic Chat sends and receives a message", async ({ + page, +}) => { + await page.goto("/openai-agents-python/feature/agentic_chat"); + + const chat = new AgenticChatPage(page); + + await chat.openChat(); + await expect(chat.agentGreeting).toBeVisible(); + await chat.sendMessage("Hi, I am Abdelrahman"); + + await chat.assertUserMessageVisible("Hi, I am Abdelrahman"); + await chat.assertAgentReplyVisible(/Hello/i); +}); + +test("[OpenAI Agents Python] Agentic Chat retains memory across turns", async ({ + page, +}) => { + await page.goto("/openai-agents-python/feature/agentic_chat"); + + const chat = new AgenticChatPage(page); + await chat.openChat(); + await expect(chat.agentGreeting).toBeVisible(); + + const favFruit = "Mango"; + await chat.sendMessage(`My favorite fruit is ${favFruit}`); + await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); + await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); + + await chat.sendMessage("Can you remind me what my favorite fruit is?"); + await chat.assertUserMessageVisible( + "Can you remind me what my favorite fruit is?", + ); + await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); +}); From 23247a9e82a4fe358bc718b38bb95348ff361471 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 05:41:02 +0200 Subject: [PATCH 86/94] feat(openai-agents): add TypeScript HttpAgent client --- .../openai-agents/typescript/.gitignore | 141 ++++++++++++++++++ integrations/openai-agents/typescript/LICENSE | 21 +++ .../openai-agents/typescript/README.md | 32 ++++ .../openai-agents/typescript/package.json | 60 ++++++++ .../openai-agents/typescript/src/index.ts | 3 + .../openai-agents/typescript/tsconfig.json | 24 +++ .../openai-agents/typescript/tsdown.config.ts | 12 ++ .../openai-agents/typescript/vitest.config.ts | 10 ++ pnpm-lock.yaml | 25 ++++ 9 files changed, 328 insertions(+) create mode 100644 integrations/openai-agents/typescript/.gitignore create mode 100644 integrations/openai-agents/typescript/LICENSE create mode 100644 integrations/openai-agents/typescript/README.md create mode 100644 integrations/openai-agents/typescript/package.json create mode 100644 integrations/openai-agents/typescript/src/index.ts create mode 100644 integrations/openai-agents/typescript/tsconfig.json create mode 100644 integrations/openai-agents/typescript/tsdown.config.ts create mode 100644 integrations/openai-agents/typescript/vitest.config.ts diff --git a/integrations/openai-agents/typescript/.gitignore b/integrations/openai-agents/typescript/.gitignore new file mode 100644 index 0000000000..0ccb8df8de --- /dev/null +++ b/integrations/openai-agents/typescript/.gitignore @@ -0,0 +1,141 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ diff --git a/integrations/openai-agents/typescript/LICENSE b/integrations/openai-agents/typescript/LICENSE new file mode 100644 index 0000000000..b77bf2ab72 --- /dev/null +++ b/integrations/openai-agents/typescript/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/integrations/openai-agents/typescript/README.md b/integrations/openai-agents/typescript/README.md new file mode 100644 index 0000000000..6e6a814817 --- /dev/null +++ b/integrations/openai-agents/typescript/README.md @@ -0,0 +1,32 @@ +# @ag-ui/openai-agents + +TypeScript client for connecting an AG-UI front end to a server built with +the companion Python integration +([`ag-ui-openai-agents`](../python/README.md)), which bridges the +[OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) and the +AG-UI protocol. + +## Status + +`OpenAIAgentsHttpAgent` is currently an empty subclass of `HttpAgent` — it +adds no behavior beyond what `@ag-ui/client`'s `HttpAgent` already provides, +since the Python side exposes no extra endpoints (e.g. no capability +discovery) for a client to call. It is marked `"private": true` and is not +published to npm. + +## Usage + +```typescript +import { OpenAIAgentsHttpAgent } from "@ag-ui/openai-agents"; + +const agent = new OpenAIAgentsHttpAgent({ + url: "http://localhost:8024/agentic_chat/", +}); + +// Use the agent with AG-UI clients +``` + +## Requirements + +- An `ag-ui-openai-agents` Python server running (see `../python/README.md`) +- AG-UI compatible client diff --git a/integrations/openai-agents/typescript/package.json b/integrations/openai-agents/typescript/package.json new file mode 100644 index 0000000000..7b7981f37a --- /dev/null +++ b/integrations/openai-agents/typescript/package.json @@ -0,0 +1,60 @@ +{ + "name": "@ag-ui/openai-agents", + "//": "private: @ag-ui/openai-agents is currently an empty HttpAgent subclass with no added behavior — intentionally NOT published. Mirrors integrations/langroid/typescript's precedent. See src/index.ts.", + "private": true, + "version": "0.0.1", + "description": "AG-UI integration for the OpenAI Agents SDK — thin TypeScript client for the companion Python integration (ag-ui-openai-agents)", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "dist/**", + "README.md" + ], + "keywords": [ + "ag-ui", + "openai", + "openai-agents", + "agents", + "ai" + ], + "scripts": { + "build": "tsdown", + "dev": "tsdown --watch", + "clean": "git clean -fdX --exclude=\"!.env\"", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest", + "link:global": "pnpm link --global", + "unlink:global": "pnpm unlink --global" + }, + "peerDependencies": { + "@ag-ui/core": ">=0.0.57", + "@ag-ui/client": ">=0.0.57", + "rxjs": "7.8.1" + }, + "devDependencies": { + "@ag-ui/core": "workspace:*", + "@ag-ui/client": "workspace:*", + "@types/node": "^20.11.19", + "tsdown": "^0.20.1", + "typescript": "^5.3.3", + "vitest": "^4.0.18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ag-ui-protocol/ag-ui.git", + "directory": "integrations/openai-agents/typescript" + }, + "author": "Abdelrahman Abozied ", + "license": "MIT", + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "./package.json": "./package.json" + } +} diff --git a/integrations/openai-agents/typescript/src/index.ts b/integrations/openai-agents/typescript/src/index.ts new file mode 100644 index 0000000000..01e2af96ac --- /dev/null +++ b/integrations/openai-agents/typescript/src/index.ts @@ -0,0 +1,3 @@ +import { HttpAgent } from "@ag-ui/client"; + +export class OpenAIAgentsHttpAgent extends HttpAgent {} diff --git a/integrations/openai-agents/typescript/tsconfig.json b/integrations/openai-agents/typescript/tsconfig.json new file mode 100644 index 0000000000..d12ec063d5 --- /dev/null +++ b/integrations/openai-agents/typescript/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", + "skipLibCheck": true, + "strict": true, + "jsx": "react-jsx", + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "stripInternal": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/integrations/openai-agents/typescript/tsdown.config.ts b/integrations/openai-agents/typescript/tsdown.config.ts new file mode 100644 index 0000000000..6f3030ec48 --- /dev/null +++ b/integrations/openai-agents/typescript/tsdown.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + exports: true, + fixedExtension: false, + sourcemap: true, + clean: true, + minify: true, +}); diff --git a/integrations/openai-agents/typescript/vitest.config.ts b/integrations/openai-agents/typescript/vitest.config.ts new file mode 100644 index 0000000000..c85b19a3f1 --- /dev/null +++ b/integrations/openai-agents/typescript/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["**/*.test.ts"], + passWithNoTests: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10c1172d4f..123c3bd034 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1013,6 +1013,31 @@ importers: specifier: ^5.9.3 version: 5.9.3 + integrations/openai-agents/typescript: + dependencies: + rxjs: + specifier: 7.8.1 + version: 7.8.1 + devDependencies: + '@ag-ui/client': + specifier: workspace:* + version: link:../../../sdks/typescript/packages/client + '@ag-ui/core': + specifier: workspace:* + version: link:../../../sdks/typescript/packages/core + '@types/node': + specifier: ^20.11.19 + version: 20.19.21 + tsdown: + specifier: ^0.20.1 + version: 0.20.1(publint@0.3.17)(typescript@5.9.3) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) + integrations/pydantic-ai/typescript: dependencies: rxjs: From 40d84712c87de2d534f4e24aa4b7b62ccf85950c Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 13:15:34 +0200 Subject: [PATCH 87/94] test(openai-agents): add Dojo E2E coverage --- apps/dojo/e2e/aimock-setup.ts | 8 + apps/dojo/e2e/openai-agents-fixtures.ts | 192 +++++++++++++++++ .../agenticChatPage.spec.ts | 49 +++-- .../backendToolRenderingPage.spec.ts | 12 ++ .../customLifecycleEventsPage.spec.ts | 15 ++ .../docsCopilotPage.spec.ts | 16 ++ .../dynamicSystemPromptPage.spec.ts | 23 +++ .../humanInTheLoopApprovalPage.spec.ts | 22 ++ .../humanInTheLoopPage.spec.ts | 20 ++ .../subagentsPage.spec.ts | 20 ++ .../toolBasedGenUIPage.spec.ts | 14 ++ .../(v2)/custom_lifecycle_events/README.mdx | 9 +- .../(v2)/custom_lifecycle_events/page.tsx | 80 ++------ .../(v2)/human_in_the_loop_approval/page.tsx | 20 +- apps/dojo/src/files.json | 194 +++++++++--------- apps/dojo/src/menu.ts | 30 +-- .../typescript/src/index.test.ts | 19 ++ 17 files changed, 536 insertions(+), 207 deletions(-) create mode 100644 apps/dojo/e2e/openai-agents-fixtures.ts create mode 100644 apps/dojo/e2e/tests/openaiAgentsPythonTests/backendToolRenderingPage.spec.ts create mode 100644 apps/dojo/e2e/tests/openaiAgentsPythonTests/customLifecycleEventsPage.spec.ts create mode 100644 apps/dojo/e2e/tests/openaiAgentsPythonTests/docsCopilotPage.spec.ts create mode 100644 apps/dojo/e2e/tests/openaiAgentsPythonTests/dynamicSystemPromptPage.spec.ts create mode 100644 apps/dojo/e2e/tests/openaiAgentsPythonTests/humanInTheLoopApprovalPage.spec.ts create mode 100644 apps/dojo/e2e/tests/openaiAgentsPythonTests/humanInTheLoopPage.spec.ts create mode 100644 apps/dojo/e2e/tests/openaiAgentsPythonTests/subagentsPage.spec.ts create mode 100644 apps/dojo/e2e/tests/openaiAgentsPythonTests/toolBasedGenUIPage.spec.ts create mode 100644 integrations/openai-agents/typescript/src/index.test.ts diff --git a/apps/dojo/e2e/aimock-setup.ts b/apps/dojo/e2e/aimock-setup.ts index 16cd32ffaf..e899bcb5d8 100644 --- a/apps/dojo/e2e/aimock-setup.ts +++ b/apps/dojo/e2e/aimock-setup.ts @@ -2,6 +2,7 @@ import { LLMock, type ChatMessage } from "@copilotkit/aimock"; import * as path from "node:path"; import { registerA2UIRecoveryFixtures } from "./a2ui-recovery-fixtures"; import { registerA2UIADKFixtures } from "./a2ui-adk-fixtures"; +import { registerOpenAIAgentsFixtures } from "./openai-agents-fixtures"; // Configurable so parallel worktrees / runs don't collide on one aimock port. const MOCK_PORT = Number(process.env.AIMOCK_PORT) || 5555; @@ -31,6 +32,7 @@ export async function setupLLMock(): Promise { // OSS-162 A2UI recovery showcase fixtures (predicate fixtures, must precede // the generic loadFixtureFile below). registerA2UIRecoveryFixtures(mockServer); + registerOpenAIAgentsFixtures(mockServer); // Extract text from message content — handles both string and array-of-parts // (Strands SDK sends content as [{type: "text", text: "..."}]) @@ -1339,6 +1341,12 @@ export async function setupLLMock(): Promise { ); if (hasCrewExitTool && textOf(last.content) === "Crew exited") return false; + const hasOpenAIAgentsSupervisorTools = req.tools?.some((t) => + ["research_topic", "write_prose", "critique_draft"].includes( + t.function.name, + ), + ); + if (hasOpenAIAgentsSupervisorTools) return false; return true; }, }, diff --git a/apps/dojo/e2e/openai-agents-fixtures.ts b/apps/dojo/e2e/openai-agents-fixtures.ts new file mode 100644 index 0000000000..84337792ea --- /dev/null +++ b/apps/dojo/e2e/openai-agents-fixtures.ts @@ -0,0 +1,192 @@ +import { LLMock, type ChatMessage } from "@copilotkit/aimock"; + +function textOf(content: ChatMessage["content"] | undefined): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content + .filter((part) => part.type === "text" && typeof part.text === "string") + .map((part) => part.text) + .join(""); +} + +export function registerOpenAIAgentsFixtures(mockServer: LLMock): void { + const hasTool = ( + req: { tools?: { function: { name: string } }[] }, + name: string, + ) => req.tools?.some((tool) => tool.function.name === name) ?? false; + const messagesText = (messages: ChatMessage[]) => + messages.map((message) => textOf(message.content)).join("\n"); + const systemText = (messages: ChatMessage[]) => + messages + .filter((message) => message.role === "system") + .map((message) => textOf(message.content)) + .join("\n"); + + mockServer.addFixture({ + match: { + predicate: (req) => + hasTool(req, "read_ag_ui_openai_agents_docs") && + messagesText(req.messages).includes( + "test the OpenAI Agents integration", + ), + }, + response: { + toolCalls: [ + { + name: "read_ag_ui_openai_agents_docs", + arguments: JSON.stringify({ heading: "Testing" }), + }, + ], + }, + }); + + mockServer.addFixture({ + match: { + predicate: (req) => + hasTool(req, "issue_refund") && + messagesText(req.messages).includes("ORD-1001"), + }, + response: { + toolCalls: [ + { + name: "issue_refund", + arguments: JSON.stringify({ order_id: "ORD-1001" }), + }, + ], + }, + }); + + const hasSupervisorTools = (req: { + tools?: { function: { name: string } }[]; + }) => + ["research_topic", "write_prose", "critique_draft"].every((name) => + hasTool(req, name), + ); + + mockServer.addFixture({ + match: { + predicate: (req) => + systemText(req.messages).includes("You research a topic") && + !hasSupervisorTools(req), + }, + response: { + content: + "AI research facts: symbolic AI began in the 1950s; machine learning later became dominant.", + }, + }); + + mockServer.addFixture({ + match: { + predicate: (req) => + systemText(req.messages).includes("You turn bullet-point facts") && + !hasSupervisorTools(req), + }, + response: { + content: + "AI evolved from symbolic systems into modern machine learning and generative models.", + }, + }); + + mockServer.addFixture({ + match: { + predicate: (req) => + systemText(req.messages).includes("You review a draft") && + !hasSupervisorTools(req), + }, + response: { + content: + "1. Improve the opening. 2. Connect the historical eras more clearly.", + }, + }); + + mockServer.addFixture({ + match: { + predicate: (req) => { + if (!hasSupervisorTools(req)) return false; + const text = messagesText(req.messages); + return !text.includes("AI research facts"); + }, + }, + response: { + toolCalls: [ + { + name: "research_topic", + arguments: JSON.stringify({ input: "history of AI" }), + }, + ], + }, + }); + + mockServer.addFixture({ + match: { + predicate: (req) => { + if (!hasSupervisorTools(req)) return false; + const text = messagesText(req.messages); + return ( + text.includes("AI research facts") && + !text.includes("AI evolved from symbolic systems") + ); + }, + }, + response: { + toolCalls: [ + { + name: "write_prose", + arguments: JSON.stringify({ input: "AI research facts" }), + }, + ], + }, + }); + + mockServer.addFixture({ + match: { + predicate: (req) => { + if (!hasSupervisorTools(req)) return false; + const text = messagesText(req.messages); + return ( + text.includes("AI evolved from symbolic systems") && + !text.includes("Improve the opening") + ); + }, + }, + response: { + toolCalls: [ + { + name: "critique_draft", + arguments: JSON.stringify({ + input: + "AI evolved from symbolic systems into modern machine learning and generative models.", + }), + }, + ], + }, + }); + + mockServer.addFixture({ + match: { + predicate: (req) => + hasSupervisorTools(req) && + messagesText(req.messages).includes("Improve the opening"), + }, + response: { + content: + "Artificial intelligence grew from early symbolic programs into machine learning and today's generative systems.", + }, + }); + + mockServer.addFixture({ + match: { + predicate: (req) => + systemText(req.messages).includes("Always reply in Arabic"), + }, + response: { content: "مرحبًا! أنا أجيب باللغة العربية." }, + }); + + mockServer.addFixture({ + match: { + predicate: (req) => + systemText(req.messages).includes("Always reply in German"), + }, + response: { content: "Hallo! Ich antworte auf Deutsch." }, + }); +} diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/agenticChatPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/agenticChatPage.spec.ts index 458909c570..0f7b0eb6d4 100644 --- a/apps/dojo/e2e/tests/openaiAgentsPythonTests/agenticChatPage.spec.ts +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/agenticChatPage.spec.ts @@ -1,38 +1,45 @@ -import { test, expect } from "../../test-isolation-helper"; +import { test } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; -test("[OpenAI Agents Python] Agentic Chat sends and receives a message", async ({ +test("[OpenAI Agents Python] Agentic Chat sends and receives a greeting message", async ({ page, }) => { await page.goto("/openai-agents-python/feature/agentic_chat"); - const chat = new AgenticChatPage(page); - await chat.openChat(); - await expect(chat.agentGreeting).toBeVisible(); - await chat.sendMessage("Hi, I am Abdelrahman"); - - await chat.assertUserMessageVisible("Hi, I am Abdelrahman"); - await chat.assertAgentReplyVisible(/Hello/i); + await chat.sendMessage("Hi"); + await chat.assertUserMessageVisible("Hi"); + await chat.assertAgentReplyVisible(/Hello|Hi|hey/i); }); -test("[OpenAI Agents Python] Agentic Chat retains memory across turns", async ({ +test("[OpenAI Agents Python] Agentic Chat retains memory of previous questions", async ({ page, }) => { + test.slow(); await page.goto("/openai-agents-python/feature/agentic_chat"); - const chat = new AgenticChatPage(page); await chat.openChat(); - await expect(chat.agentGreeting).toBeVisible(); - - const favFruit = "Mango"; - await chat.sendMessage(`My favorite fruit is ${favFruit}`); - await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); - await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); + await chat.sendMessage("Hi, my name is Alex"); + await chat.assertUserMessageVisible("Hi, my name is Alex"); + await chat.assertAgentReplyVisible(/Hello|Hi|Alex/i); + await chat.sendMessage("What is my name?"); + await chat.assertUserMessageVisible("What is my name?"); + await chat.assertAgentReplyVisible(/Alex/i); +}); +test("[OpenAI Agents Python] Agentic Chat retains user messages during a conversation", async ({ + page, +}) => { + test.slow(); + await page.goto("/openai-agents-python/feature/agentic_chat"); + const chat = new AgenticChatPage(page); + await chat.openChat(); + await chat.sendMessage("Hey there"); + await chat.assertAgentReplyVisible(/Hello|Hi/i); + await chat.sendMessage("My favorite fruit is Mango"); + await chat.assertAgentReplyVisible(/Mango/i); + await chat.sendMessage("and I love listening to Kaavish"); + await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("Can you remind me what my favorite fruit is?"); - await chat.assertUserMessageVisible( - "Can you remind me what my favorite fruit is?", - ); - await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); + await chat.assertAgentReplyVisible(/Mango/i); }); diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/backendToolRenderingPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/backendToolRenderingPage.spec.ts new file mode 100644 index 0000000000..33f46d0b2c --- /dev/null +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/backendToolRenderingPage.spec.ts @@ -0,0 +1,12 @@ +import { expect, test } from "../../test-isolation-helper"; + +test("[OpenAI Agents Python] Backend Tool Rendering displays a weather card", async ({ + page, +}) => { + await page.goto("/openai-agents-python/feature/backend_tool_rendering"); + + await page.getByRole("button", { name: "Weather in San Francisco" }).click(); + + await expect(page.getByTestId("weather-card").last()).toBeVisible(); + await expect(page.getByText("Humidity").last()).toBeVisible(); +}); diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/customLifecycleEventsPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/customLifecycleEventsPage.spec.ts new file mode 100644 index 0000000000..ee2e10b006 --- /dev/null +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/customLifecycleEventsPage.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from "../../test-isolation-helper"; +import { sendAndAwaitResponse } from "../../utils/copilot-actions"; + +test("[OpenAI Agents Python] Custom Lifecycle Events displays real token usage", async ({ + page, +}) => { + await page.goto("/openai-agents-python/feature/custom_lifecycle_events"); + + await sendAndAwaitResponse(page, "Hi"); + + const usage = page.getByLabel("Run usage").last(); + await expect(usage).toBeVisible(); + await expect(usage).toContainText(/\d+ input tokens/); + await expect(usage).toContainText(/\d+ output tokens/); +}); diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/docsCopilotPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/docsCopilotPage.spec.ts new file mode 100644 index 0000000000..26437a48de --- /dev/null +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/docsCopilotPage.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from "../../test-isolation-helper"; +import { sendAndAwaitResponse } from "../../utils/copilot-actions"; + +test("[OpenAI Agents Python] Docs Copilot reads the integration documentation", async ({ + page, +}) => { + await page.goto("/openai-agents-python/feature/ag_ui_docs_copilot"); + + await sendAndAwaitResponse( + page, + "How do I test the OpenAI Agents integration?", + ); + + await expect(page.getByText("AG-UI OpenAI Agents docs").last()).toBeVisible(); + await expect(page.getByText('Read "Testing"').last()).toBeVisible(); +}); diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/dynamicSystemPromptPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/dynamicSystemPromptPage.spec.ts new file mode 100644 index 0000000000..3335d9f52e --- /dev/null +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/dynamicSystemPromptPage.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from "../../test-isolation-helper"; +import { sendAndAwaitResponse } from "../../utils/copilot-actions"; + +test("[OpenAI Agents Python] Dynamic System Prompt changes the reply language", async ({ + page, +}) => { + test.slow(); + await page.goto("/openai-agents-python/feature/dynamic_system_prompt"); + + await page.getByRole("button", { name: /Arabic/ }).click(); + await sendAndAwaitResponse(page, "Which language are you using?"); + const latestReply = () => + page + .locator('[data-testid="copilot-assistant-message"]') + .last() + .locator("p") + .last(); + await expect(latestReply()).toContainText(/[\u0600-\u06ff]/); + + await page.getByRole("button", { name: /German/ }).click(); + await sendAndAwaitResponse(page, "Which language are you using now?"); + await expect(latestReply()).toContainText(/deutsch/i); +}); diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/humanInTheLoopApprovalPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/humanInTheLoopApprovalPage.spec.ts new file mode 100644 index 0000000000..49872b3137 --- /dev/null +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/humanInTheLoopApprovalPage.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from "../../test-isolation-helper"; +import { + awaitLLMResponseDone, + sendChatMessage, +} from "../../utils/copilot-actions"; + +test("[OpenAI Agents Python] Approval resumes a paused refund tool", async ({ + page, +}) => { + test.slow(); + await page.goto("/openai-agents-python/feature/human_in_the_loop_approval"); + + await sendChatMessage(page, "Please refund ORD-1001."); + await awaitLLMResponseDone(page); + const card = page.getByTestId("approval-card"); + await expect(card).toBeVisible(); + await expect(card).toContainText("ORD-1001"); + + await page.getByTestId("approve-button").click(); + await awaitLLMResponseDone(page); + await expect(card).toBeHidden(); +}); diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/humanInTheLoopPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/humanInTheLoopPage.spec.ts new file mode 100644 index 0000000000..2bf6cc9674 --- /dev/null +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/humanInTheLoopPage.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from "../../test-isolation-helper"; +import { HumanInTheLoopPage } from "../../featurePages/HumanInTheLoopPage"; + +test("[OpenAI Agents Python] Human in the Loop renders and submits selected steps", async ({ + page, +}) => { + test.slow(); + await page.goto("/openai-agents-python/feature/human_in_the_loop"); + + const humanInLoop = new HumanInTheLoopPage(page); + await humanInLoop.openChat(); + await humanInLoop.sendMessage( + "Give me a plan to make brownies, there should be only one step with eggs and one step with oven", + ); + + await expect(humanInLoop.plan).toBeVisible(); + await humanInLoop.uncheckItem("eggs"); + expect(await humanInLoop.isStepItemUnchecked("eggs")).toBe(true); + await humanInLoop.performSteps(); +}); diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/subagentsPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/subagentsPage.spec.ts new file mode 100644 index 0000000000..c8a44ab7f0 --- /dev/null +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/subagentsPage.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from "../../test-isolation-helper"; +import { sendAndAwaitResponse } from "../../utils/copilot-actions"; + +test("[OpenAI Agents Python] Subagents records all specialist delegations", async ({ + page, +}) => { + test.slow(); + await page.goto("/openai-agents-python/feature/subagents"); + + await sendAndAwaitResponse( + page, + "Write a short article about the history of AI", + ); + + const log = page.getByText("Delegation log").locator(".."); + await expect(log.getByText("Researcher").last()).toBeVisible(); + await expect(log.getByText("Writer").last()).toBeVisible(); + await expect(log.getByText("Critic").last()).toBeVisible(); + await expect(log.getByText("complete")).toHaveCount(3); +}); diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/toolBasedGenUIPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/toolBasedGenUIPage.spec.ts new file mode 100644 index 0000000000..f5f047fe66 --- /dev/null +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/toolBasedGenUIPage.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from "../../test-isolation-helper"; +import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; + +test("[OpenAI Agents Python] Tool Based Generative UI renders a haiku", async ({ + page, +}) => { + await page.goto("/openai-agents-python/feature/tool_based_generative_ui"); + + const haiku = new ToolBaseGenUIPage(page); + await expect(haiku.haikuAgentIntro).toBeVisible(); + await haiku.generateHaiku('Generate Haiku for "I will always win"'); + await haiku.checkGeneratedHaiku(); + await haiku.checkHaikuDisplay(page); +}); diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx index 8a3a2af9d2..b740c0c0da 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx @@ -20,8 +20,7 @@ demonstration bracketing pair would. - "Say hi in one sentence." - "Tell me one interesting fact about AI." -Each assistant reply has a compact usage label immediately above it, showing -the real input and output token counts for that run. +The latest run's real input and output token counts appear above the chat. ## Technical Details @@ -37,7 +36,5 @@ the real input and output token counts for that run. - The translator itself is opaque to the event's contents — it only checks `isinstance(..., CustomEvent)`; the `name`/`value` shape is entirely up to the caller -- The page captures the current `runId` from `RUN_STARTED`, associates the - `run_usage` event with that run in a small external store, then uses - `CopilotKitProvider`'s stable `renderCustomMessages` configuration to render - the label above the assistant message +- The page subscribes to `run_usage`, stores the latest value in local React + state, and renders it above the chat diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx index fdf79c178a..46b6e28676 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx @@ -1,13 +1,13 @@ "use client"; -import React, { useEffect, useMemo, useSyncExternalStore } from "react"; +import React, { useEffect, useState } from "react"; import "@copilotkit/react-core/v2/styles.css"; import { CopilotChat, - CopilotKitProvider, useAgent, useConfigureSuggestions, } from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; interface CustomLifecycleEventsProps { params: Promise<{ integrationId: string }>; @@ -18,73 +18,25 @@ interface RunUsage { outputTokens: number; } -// Keyed by the assistant message's own id rather than the framework's -// run id: CopilotKit's message-to-run mapping only resolves reliably for -// the first run in a session, so a run-id-keyed store silently drops usage -// on every later turn. Message ids don't have that problem. -const usageByMessageId = new Map(); -const usageListeners = new Set<() => void>(); - -function setMessageUsage(messageId: string, usage: RunUsage) { - usageByMessageId.set(messageId, usage); - usageListeners.forEach((listener) => listener()); -} - -function subscribeUsage(listener: () => void) { - usageListeners.add(listener); - return () => usageListeners.delete(listener); -} - -function getMessageUsage(messageId: string) { - return usageByMessageId.get(messageId); -} - -function UsageLabel({ - message, - position, -}: { - message: { id: string; role: string }; - position: "before" | "after"; -}) { - const usage = useSyncExternalStore( - subscribeUsage, - () => getMessageUsage(message.id), - () => undefined, - ); - if (message.role !== "assistant" || position !== "after" || !usage) { - return null; - } - - return ( -
- ⬇️ {usage.inputTokens} input tokens · ⬆️ {usage.outputTokens} output - tokens -
- ); -} - const CustomLifecycleEvents: React.FC = ({ params, }) => { const { integrationId } = React.use(params); - const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []); return ( - - + ); }; const Chat = () => { const { agent } = useAgent({ agentId: "custom_lifecycle_events" }); + const [usage, setUsage] = useState(null); useConfigureSuggestions({ suggestions: [ @@ -100,18 +52,13 @@ const Chat = () => { useEffect(() => { const subscription = agent.subscribe({ - onCustomEvent: ({ event, messages }) => { + onCustomEvent: ({ event }) => { if (event.name !== "run_usage") return; - // run_usage fires after MESSAGES_SNAPSHOT, so the assistant's - // reply is already the last message in this run's history. - const lastMessage = messages[messages.length - 1]; - if (!lastMessage) return; - const value = event.value as { input_tokens: number; output_tokens: number; }; - setMessageUsage(lastMessage.id, { + setUsage({ inputTokens: value.input_tokens, outputTokens: value.output_tokens, }); @@ -121,7 +68,16 @@ const Chat = () => { }, [agent]); return ( -
+
+ {usage && ( +
+ ⬇️ {usage.inputTokens} input tokens · ⬆️ {usage.outputTokens} output + tokens +
+ )}
; @@ -23,12 +23,13 @@ const Approval: React.FC = ({ params }) => { const { integrationId } = React.use(params); return ( - - + ); }; @@ -90,10 +91,16 @@ const Chat = () => { return (
{pending && ( - respond(true)} onReject={() => respond(false)} /> + respond(true)} + onReject={() => respond(false)} + /> )} {!pending && resolvedCallId && ( -
Decision sent — waiting for the agent...
+
+ Decision sent — waiting for the agent... +
)}

Approval needed

- The agent wants to call {pending.toolName} with: + The agent wants to call{" "} + {pending.toolName} with:

         {JSON.stringify(args, null, 2)}
diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json
index 0a5f16be74..c74d43a6d0 100644
--- a/apps/dojo/src/files.json
+++ b/apps/dojo/src/files.json
@@ -4813,33 +4813,39 @@
       "type": "file"
     }
   ],
-  "openai-agents-python::ag_ui_docs_copilot": [
+  "langroid::agentic_chat": [
     {
       "name": "page.tsx",
-      "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n  CopilotChat,\n  useConfigureSuggestions,\n  useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n  params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n  const { integrationId } = React.use(params);\n\n  return (\n    \n      \n    \n  );\n};\n\nconst DocsChat = () => {\n  useRenderTool({\n    name: \"read_ag_ui_openai_agents_docs\",\n    agentId: \"ag_ui_docs_copilot\",\n    parameters: z.object({ heading: z.string().optional() }),\n    render: ({ status, args, result }: any) => (\n      \n    ),\n  });\n  useRenderTool({\n    name: \"read_ag_ui_protocol_docs\",\n    agentId: \"ag_ui_docs_copilot\",\n    parameters: z.object({ heading: z.string().optional() }),\n    render: ({ status, args, result }: any) => (\n      \n    ),\n  });\n\n  useConfigureSuggestions({\n    suggestions: [\n      {\n        title: \"Stream an existing agent\",\n        message:\n          \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n      },\n      {\n        title: \"Map SDK events to AG-UI\",\n        message:\n          \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n      },\n      {\n        title: \"Choose an integration layer\",\n        message:\n          \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n      },\n    ],\n    available: \"always\",\n  });\n\n  return (\n    
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n source,\n status,\n heading,\n result,\n}: {\n source: \"AG-UI OpenAI Agents\" | \"AG-UI Protocol\";\n status: \"inProgress\" | \"executing\" | \"complete\";\n heading?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const missed = complete && !!result && result.startsWith(\"No section matches\");\n\n return (\n
\n \n {missed ? \"!\" : complete ? \"✓\" : \"📖\"}\n \n
\n
\n \n {source} docs\n \n {!complete && (\n \n \n \n •\n \n \n •\n \n \n )}\n
\n
\n {heading\n ? `${complete ? \"Read\" : \"Reading\"} \"${heading}\"`\n : complete\n ? \"Section read\"\n : \"Opening the guide\"}\n
\n
\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useRenderTool,\n useAgentContext,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n\n useAgentContext({\n description: 'Name of the user',\n value: 'Bob'\n });\n\n useFrontendTool({\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients. Only use when asked.\"),\n }) ,\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n return {\n status: \"success\",\n message: `Background changed to ${background}`,\n };\n },\n });\n\n useRenderTool({\n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return
Loading weather...
;\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field reads as\n // undefined and the card renders empty values.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n return (\n
\n Weather in {parsed.city ?? args.location}\n
Temperature: {parsed.temperature}°C
\n
Humidity: {parsed.humidity}%
\n
Wind Speed: {parsed.windSpeed ?? parsed.wind_speed} mph
\n
Conditions: {parsed.conditions}
\n
\n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Change background\",\n message: \"Change the background to something new.\",\n },\n {\n title: \"Generate sonnet\",\n message: \"Write a short sonnet about AI.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n \n
\n \n
\n
\n );\n};\n\nexport default AgenticChat;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nOne **Copilot** agent handles normal conversation directly, and for AG-UI or\nOpenAI Agents SDK questions calls one of two tools, `read_ag_ui_protocol_docs`\nand `read_ag_ui_openai_agents_docs`, to read a single section of the relevant\nlocal README by heading.\n\n## What it demonstrates\n\n```text\nCopilot → read_ag_ui_protocol_docs / read_ag_ui_openai_agents_docs (local READMEs, one section at a time)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- Table-of-contents instructions plus an on-demand section-read tool, instead\n of loading a whole README into the prompt\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded by section\n\nThe integration README is small enough for this focused demo, so its heading\nlist sits in the Copilot's instructions and each tool call reads back one\nmatching section. This keeps the example deterministic, cheap, and fast to\nstream — an earlier version routed each question through a second\nsub-agent, which cost extra model round trips before the first token showed\nup. A production assistant with many or large documents should replace this\nwith its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask for a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", + "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", "language": "markdown", "type": "file" }, { - "name": "ag_ui_docs_copilot.py", - "content": "\"\"\"AG-UI documentation assistant that reads local README sections on demand.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom importlib.metadata import metadata\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n_HEADING_RE = re.compile(r\"^#{1,6}\\s+(.+)$\")\n\n\ndef _load_distribution_readme(distribution: str) -> str:\n \"\"\"Load the installed package README from its distribution metadata.\"\"\"\n document = metadata(distribution).get_payload()\n if not isinstance(document, str) or not document.strip():\n raise RuntimeError(f\"{distribution} has no README in its package metadata\")\n return document\n\n\nclass MarkdownSections:\n \"\"\"A Markdown document split by heading, read one section at a time.\n\n The Copilot sees only the heading list in its instructions and fetches\n the sections it needs on demand, so the full document never sits in a\n prompt.\n \"\"\"\n\n def __init__(self, document: str) -> None:\n sections: dict[str, list[str]] = {}\n heading = \"Overview\"\n for line in document.splitlines():\n match = _HEADING_RE.match(line)\n if match:\n heading = match.group(1).strip()\n sections.setdefault(heading, []).append(line)\n self._sections = {\n name: content\n for name, lines in sections.items()\n if (content := \"\\n\".join(lines).strip())\n }\n\n @property\n def headings(self) -> str:\n \"\"\"The table of contents: one heading per line.\"\"\"\n return \"\\n\".join(f\"- {name}\" for name in self._sections)\n\n def read(self, heading: str) -> str:\n \"\"\"Return one section by heading (case-insensitive, substring ok).\"\"\"\n wanted = heading.strip().lstrip(\"#\").strip().lower()\n for name, content in self._sections.items():\n if name.lower() == wanted:\n return content\n for name, content in self._sections.items():\n if wanted in name.lower():\n return content\n return f\"No section matches {heading!r}. Available headings:\\n{self.headings}\"\n\n\n# ag-ui-protocol docs: the core Python SDK README.\nAG_UI_PROTOCOL_DOCS = _load_distribution_readme(\"ag-ui-protocol\")\n_AG_UI_PROTOCOL_SECTIONS = MarkdownSections(AG_UI_PROTOCOL_DOCS)\n\n\n@function_tool\ndef read_ag_ui_protocol_docs(heading: str) -> str:\n \"\"\"Read one AG-UI Protocol Python documentation section by its heading.\n\n Args:\n heading: A heading from the \"AG-UI Protocol docs\" table of contents\n in your instructions.\n \"\"\"\n return _AG_UI_PROTOCOL_SECTIONS.read(heading)\n\n\n# ag-ui-openai-agents docs: this integration's README.\nAG_UI_OPENAI_AGENTS_DOCS = _load_distribution_readme(\"ag-ui-openai-agents\")\n_AG_UI_OPENAI_AGENTS_SECTIONS = MarkdownSections(AG_UI_OPENAI_AGENTS_DOCS)\n\n\n@function_tool\ndef read_ag_ui_openai_agents_docs(heading: str) -> str:\n \"\"\"Read one AG-UI OpenAI Agents integration documentation section.\n\n Args:\n heading: A heading from the \"AG-UI OpenAI Agents docs\" table of\n contents in your instructions.\n \"\"\"\n return _AG_UI_OPENAI_AGENTS_SECTIONS.read(heading)\n\n\ncopilot_instructions = f\"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question about AG-UI, the OpenAI Agents SDK, translator APIs, FastAPI\nendpoints, streaming, tools, client tools, state, lifecycle events, errors,\ntests, or Python implementation, pick the most relevant heading from the\nmatching table of contents below and call the matching tool before answering.\nUse read_ag_ui_openai_agents_docs for OpenAI Agents SDK integration questions\nand read_ag_ui_protocol_docs for ag_ui.core protocol types and EventEncoder\nquestions. Treat the returned section as your only source of truth. Read\nanother section if the first one is not enough.\n\nUse only the part of the section that answers the user's question. Do not\ninvent behavior or claim details the documentation did not provide. For code\nrequests, provide only the smallest complete, production-readable Python\nsnippet needed, followed by a short explanation. Use documented APIs only.\n\nAG-UI Protocol docs table of contents:\n{_AG_UI_PROTOCOL_SECTIONS.headings}\n\nAG-UI OpenAI Agents docs table of contents:\n{_AG_UI_OPENAI_AGENTS_SECTIONS.headings}\n\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[read_ag_ui_protocol_docs, read_ag_ui_openai_agents_docs],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", + "name": "agentic_chat.py", + "content": "\"\"\"Agentic Chat example for Langroid.\n\nSimple conversational agent with change_background frontend tool.\n\"\"\"\nimport os\nfrom pathlib import Path\nfrom dotenv import load_dotenv\n\nenv_path = Path(__file__).parent.parent.parent / '.env'\nload_dotenv(dotenv_path=env_path)\n\nimport langroid as lr\nfrom langroid.agent import ToolMessage\nfrom langroid.language_models import OpenAIChatModel\nfrom ag_ui_langroid import LangroidAgent, create_langroid_app\n\n\nclass ChangeBackgroundTool(ToolMessage):\n request: str = \"change_background\"\n purpose: str = \"\"\"\n Change the background color of the chat. Can be anything that the CSS background\n attribute accepts. Regular colors, linear or radial gradients etc.\n Only use when the user explicitly asks to change the background.\n \"\"\"\n background: str\n\nllm_config = lr.language_models.OpenAIGPTConfig(\n chat_model=OpenAIChatModel.GPT4_1_MINI,\n api_key=os.getenv(\"OPENAI_API_KEY\"),\n temperature=0.0,\n)\n\nagent_config = lr.ChatAgentConfig(\n name=\"Assistant\",\n llm=llm_config,\n system_message=\"\"\"You are a helpful assistant. \nWhen you change the background, always confirm the action to the user with a friendly message like 'I've changed the background to [color/gradient] for you!' or similar.\"\"\",\n use_tools=True,\n use_functions_api=True,\n)\n\nchat_agent = lr.ChatAgent(agent_config)\nchat_agent.enable_message(ChangeBackgroundTool)\n\ntask = lr.Task(\n chat_agent,\n name=\"Assistant\",\n interactive=False,\n single_round=False,\n)\n\nagui_agent = LangroidAgent(\n agent=task,\n name=\"agentic_chat\",\n description=\"Simple conversational Langroid agent with frontend tools\",\n)\n\napp = create_langroid_app(agui_agent, \"/\")\n\n", "language": "python", "type": "file" } ], - "openai-agents-python::agentic_chat": [ + "langroid::backend_tool_rendering": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useRenderTool,\n useAgentContext,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n\n useAgentContext({\n description: 'Name of the user',\n value: 'Bob'\n });\n\n useFrontendTool({\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients. Only use when asked.\"),\n }) ,\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n return {\n status: \"success\",\n message: `Background changed to ${background}`,\n };\n },\n });\n\n useRenderTool({\n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return
Loading weather...
;\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field reads as\n // undefined and the card renders empty values.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n return (\n
\n Weather in {parsed.city ?? args.location}\n
Temperature: {parsed.temperature}°C
\n
Humidity: {parsed.humidity}%
\n
Wind Speed: {parsed.windSpeed ?? parsed.wind_speed} mph
\n
Conditions: {parsed.conditions}
\n
\n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Change background\",\n message: \"Change the background to something new.\",\n },\n {\n title: \"Generate sonnet\",\n message: \"Write a short sonnet about AI.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n \n
\n \n
\n
\n );\n};\n\nexport default AgenticChat;\n", + "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { \n useRenderTool,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n useRenderTool({\n \n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return (\n
\n ⚙️ Retrieving weather...\n
\n );\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field falls\n // through to its `|| 0` default and the card shows 0° C.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n const weatherResult: WeatherToolResult = {\n temperature: parsed.temperature ?? 0,\n conditions: parsed.conditions ?? \"clear\",\n humidity: parsed.humidity ?? 0,\n windSpeed: parsed.wind_speed ?? parsed.windSpeed ?? 0,\n feelsLike:\n parsed.feels_like ?? parsed.feelsLike ?? parsed.temperature ?? 0,\n };\n\n const themeColor = getThemeColor(weatherResult.conditions);\n\n return (\n \n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Weather in San Francisco\",\n message: \"What's the weather like in San Francisco?\",\n },\n {\n title: \"Weather in New York\",\n message: \"Tell me about the weather in New York.\",\n },\n {\n title: \"Weather in Tokyo\",\n message: \"How's the weather in Tokyo today?\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\ninterface WeatherToolResult {\n temperature: number;\n conditions: string;\n humidity: number;\n windSpeed: number;\n feelsLike: number;\n}\n\nfunction getThemeColor(conditions: string): string {\n const conditionLower = conditions.toLowerCase();\n if (conditionLower.includes(\"clear\") || conditionLower.includes(\"sunny\")) {\n return \"#667eea\";\n }\n if (conditionLower.includes(\"rain\") || conditionLower.includes(\"storm\")) {\n return \"#4A5568\";\n }\n if (conditionLower.includes(\"cloud\")) {\n return \"#718096\";\n }\n if (conditionLower.includes(\"snow\")) {\n return \"#63B3ED\";\n }\n return \"#764ba2\";\n}\n\nfunction WeatherCard({\n location,\n themeColor,\n result,\n status,\n}: {\n location?: string;\n themeColor: string;\n result: WeatherToolResult;\n status: \"inProgress\" | \"executing\" | \"complete\";\n}) {\n return (\n \n
\n
\n
\n

\n {location}\n

\n

Current Weather

\n
\n \n
\n\n
\n
\n {result.temperature}° C\n \n {\" / \"}\n {((result.temperature * 9) / 5 + 32).toFixed(1)}° F\n \n
\n
{result.conditions}
\n
\n\n
\n
\n
\n

Humidity

\n

{result.humidity}%

\n
\n
\n

Wind

\n

{result.windSpeed} mph

\n
\n
\n

Feels Like

\n

{result.feelsLike}°

\n
\n
\n
\n
\n
\n );\n}\n\nfunction WeatherIcon({ conditions }: { conditions: string }) {\n if (!conditions) return null;\n\n if (conditions.toLowerCase().includes(\"clear\") || conditions.toLowerCase().includes(\"sunny\")) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"rain\") ||\n conditions.toLowerCase().includes(\"drizzle\") ||\n conditions.toLowerCase().includes(\"snow\") ||\n conditions.toLowerCase().includes(\"thunderstorm\")\n ) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"fog\") ||\n conditions.toLowerCase().includes(\"cloud\") ||\n conditions.toLowerCase().includes(\"overcast\")\n ) {\n return ;\n }\n\n return ;\n}\n\n// Simple sun icon for the weather card\nfunction SunIcon() {\n return (\n \n \n \n \n );\n}\n\nfunction RainIcon() {\n return (\n \n {/* Cloud */}\n \n {/* Rain drops */}\n \n \n );\n}\n\nfunction CloudIcon() {\n return (\n \n \n \n );\n}\n\nexport default AgenticChat;\n", "language": "typescript", "type": "file" }, + { + "name": "style.css", + "content": ".copilotKitInput {\n border-bottom-left-radius: 0.75rem;\n border-bottom-right-radius: 0.75rem;\n border-top-left-radius: 0.75rem;\n border-top-right-radius: 0.75rem;\n border: 1px solid var(--copilot-kit-separator-color) !important;\n}\n\n.copilotKitChat {\n background-color: #fff !important;\n}\n", + "language": "css", + "type": "file" + }, { "name": "README.mdx", "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", @@ -4847,16 +4853,16 @@ "type": "file" }, { - "name": "agentic_chat.py", - "content": "\"\"\"Agentic chat — plain conversation, no tools.\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_agentic_chat_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\nagent = OpenAIAgentsAgent(create_agentic_chat_agent(), name=\"agentic_chat\")\napp = FastAPI(title=\"Agentic chat AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "name": "backend_tool_rendering.py", + "content": "\"\"\"Backend Tool Rendering example for Langroid.\n\nThis example shows an agent with backend tool rendering capabilities.\nBackend tools are executed on the server side, and the results are returned to the agent.\n\"\"\"\nimport json\nimport os\nimport random\nfrom pathlib import Path\nfrom dotenv import load_dotenv\n\nenv_path = Path(__file__).parent.parent.parent / '.env'\nload_dotenv(dotenv_path=env_path)\n\nimport langroid as lr\nfrom langroid.agent import ToolMessage, ChatAgent\nfrom langroid.language_models import OpenAIChatModel\nfrom ag_ui_langroid import LangroidAgent, create_langroid_app\n\n\nclass GetWeatherTool(ToolMessage):\n \"\"\"Get weather information for a location.\"\"\"\n request: str = \"get_weather\"\n purpose: str = \"\"\"\n Get current weather information for a specific location.\n Use this when the user asks about weather conditions.\n \"\"\"\n location: str\n\nclass RenderChartTool(ToolMessage):\n \"\"\"Render a chart with backend processing.\"\"\"\n request: str = \"render_chart\"\n purpose: str = \"\"\"\n Render a chart with backend processing capabilities.\n Use this when the user wants to visualize data in a chart format.\n \"\"\"\n chart_type: str\n data: str\n\n\nllm_config = lr.language_models.OpenAIGPTConfig(\n chat_model=OpenAIChatModel.GPT4_1_MINI,\n api_key=os.getenv(\"OPENAI_API_KEY\"),\n # Make behavior deterministic for demos and e2e tests\n temperature=0.0,\n)\n\n\n\nagent_config = lr.ChatAgentConfig(\n name=\"WeatherAssistant\",\n llm=llm_config,\n system_message=\"\"\"You are a helpful assistant with backend tool rendering capabilities.\n You can get weather information and render charts.\n\n CRITICAL RULES:\n - When the user asks about the weather for a specific location, you MUST call the `get_weather` tool EXACTLY ONCE.\n - Do NOT answer with current weather details unless you have first called `get_weather` and used the returned JSON.\n - When describing weather data, use the EXACT values from the tool result (temperature, conditions, humidity, wind speed, feels_like, location).\n - Never tell the user you are going to fetch or retrieve weather data without actually calling the `get_weather` tool.\n - When the user asks to visualize or chart data, you MUST call the `render_chart` tool to generate the chart metadata.\n - After calling a tool, provide a brief natural-language summary that is fully consistent with the tool result.\n \"\"\",\n use_tools=True,\n use_functions_api=True,\n)\n\n\nclass WeatherAssistantAgent(ChatAgent):\n \"\"\"ChatAgent with backend tool handlers.\"\"\"\n \n def get_weather(self, msg: GetWeatherTool) -> str:\n \"\"\"Handle get_weather tool execution. Returns JSON string with weather data.\"\"\"\n location = msg.location\n conditions_list = [\"sunny\", \"cloudy\", \"rainy\", \"clear\", \"partly cloudy\"]\n result = {\n \"temperature\": random.randint(60, 85),\n \"conditions\": random.choice(conditions_list),\n \"humidity\": random.randint(30, 80),\n \"wind_speed\": random.randint(5, 20),\n \"feels_like\": random.randint(58, 88),\n \"location\": location\n }\n return json.dumps(result)\n \n def render_chart(self, msg: RenderChartTool) -> str:\n \"\"\"Handle render_chart tool execution. Returns JSON string with chart data.\"\"\"\n chart_type = msg.chart_type\n data = msg.data\n result = {\n \"chart_type\": chart_type,\n \"data_preview\": data[:100] if len(data) > 100 else data,\n \"status\": \"rendered\",\n \"message\": f\"Successfully rendered {chart_type} chart\"\n }\n return json.dumps(result)\n\n\nchat_agent = WeatherAssistantAgent(agent_config)\nchat_agent.enable_message(GetWeatherTool)\nchat_agent.enable_message(RenderChartTool)\n\ntask = lr.Task(\n chat_agent,\n name=\"WeatherAssistant\",\n interactive=False,\n single_round=False,\n)\n\nagui_agent = LangroidAgent(\n agent=task,\n name=\"backend_tool_rendering\",\n description=\"Langroid agent with backend tool rendering support - weather and chart rendering\",\n)\n\napp = create_langroid_app(agui_agent, \"/\")\n\n", "language": "python", "type": "file" } ], - "openai-agents-python::backend_tool_rendering": [ + "langroid::agentic_generative_ui": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { \n useRenderTool,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n useRenderTool({\n \n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return (\n
\n ⚙️ Retrieving weather...\n
\n );\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field falls\n // through to its `|| 0` default and the card shows 0° C.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n const weatherResult: WeatherToolResult = {\n temperature: parsed.temperature ?? 0,\n conditions: parsed.conditions ?? \"clear\",\n humidity: parsed.humidity ?? 0,\n windSpeed: parsed.wind_speed ?? parsed.windSpeed ?? 0,\n feelsLike:\n parsed.feels_like ?? parsed.feelsLike ?? parsed.temperature ?? 0,\n };\n\n const themeColor = getThemeColor(weatherResult.conditions);\n\n return (\n \n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Weather in San Francisco\",\n message: \"What's the weather like in San Francisco?\",\n },\n {\n title: \"Weather in New York\",\n message: \"Tell me about the weather in New York.\",\n },\n {\n title: \"Weather in Tokyo\",\n message: \"How's the weather in Tokyo today?\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\ninterface WeatherToolResult {\n temperature: number;\n conditions: string;\n humidity: number;\n windSpeed: number;\n feelsLike: number;\n}\n\nfunction getThemeColor(conditions: string): string {\n const conditionLower = conditions.toLowerCase();\n if (conditionLower.includes(\"clear\") || conditionLower.includes(\"sunny\")) {\n return \"#667eea\";\n }\n if (conditionLower.includes(\"rain\") || conditionLower.includes(\"storm\")) {\n return \"#4A5568\";\n }\n if (conditionLower.includes(\"cloud\")) {\n return \"#718096\";\n }\n if (conditionLower.includes(\"snow\")) {\n return \"#63B3ED\";\n }\n return \"#764ba2\";\n}\n\nfunction WeatherCard({\n location,\n themeColor,\n result,\n status,\n}: {\n location?: string;\n themeColor: string;\n result: WeatherToolResult;\n status: \"inProgress\" | \"executing\" | \"complete\";\n}) {\n return (\n \n
\n
\n
\n

\n {location}\n

\n

Current Weather

\n
\n \n
\n\n
\n
\n {result.temperature}° C\n \n {\" / \"}\n {((result.temperature * 9) / 5 + 32).toFixed(1)}° F\n \n
\n
{result.conditions}
\n
\n\n
\n
\n
\n

Humidity

\n

{result.humidity}%

\n
\n
\n

Wind

\n

{result.windSpeed} mph

\n
\n
\n

Feels Like

\n

{result.feelsLike}°

\n
\n
\n
\n
\n
\n );\n}\n\nfunction WeatherIcon({ conditions }: { conditions: string }) {\n if (!conditions) return null;\n\n if (conditions.toLowerCase().includes(\"clear\") || conditions.toLowerCase().includes(\"sunny\")) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"rain\") ||\n conditions.toLowerCase().includes(\"drizzle\") ||\n conditions.toLowerCase().includes(\"snow\") ||\n conditions.toLowerCase().includes(\"thunderstorm\")\n ) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"fog\") ||\n conditions.toLowerCase().includes(\"cloud\") ||\n conditions.toLowerCase().includes(\"overcast\")\n ) {\n return ;\n }\n\n return ;\n}\n\n// Simple sun icon for the weather card\nfunction SunIcon() {\n return (\n \n \n \n \n );\n}\n\nfunction RainIcon() {\n return (\n \n {/* Cloud */}\n \n {/* Rain drops */}\n \n \n );\n}\n\nfunction CloudIcon() {\n return (\n \n \n \n );\n}\n\nexport default AgenticChat;\n", + "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { \n useAgent,\n UseAgentUpdate,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { useTheme } from \"next-themes\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticGenerativeUIProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticGenerativeUI: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n return (\n \n \n \n );\n};\n\ninterface AgentState {\n steps: {\n description: string;\n status: \"pending\" | \"completed\";\n }[];\n}\n\nconst Chat = () => {\n const { theme } = useTheme();\n const { agent } = useAgent({\n agentId: \"agentic_generative_ui\",\n updates: [UseAgentUpdate.OnStateChanged],\n });\n\n const agentState = agent.state as AgentState | undefined;\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Simple plan\",\n message: \"Please build a plan to go to mars in 5 steps.\",\n },\n {\n title: \"Complex plan\",\n message: \"Please build a plan to go to make pizza in 10 steps.\",\n },\n ],\n available: \"always\",\n });\n\n const steps = agentState?.steps;\n\n return (\n
\n
\n (\n
\n {messageElements}\n {steps && steps.length > 0 && (\n
\n \n
\n )}\n {interruptElement}\n
\n ),\n }}\n />\n
\n
\n );\n};\n\nfunction TaskProgress({ steps, theme }: { steps: AgentState[\"steps\"]; theme?: string }) {\n const completedCount = steps.filter((step) => step.status === \"completed\").length;\n const progressPercentage = (completedCount / steps.length) * 100;\n\n return (\n
\n \n {/* Header */}\n
\n
\n

\n Task Progress\n

\n
\n {completedCount}/{steps.length} Complete\n
\n
\n\n {/* Progress Bar */}\n \n \n \n
\n
\n\n {/* Steps */}\n
\n {steps.map((step, index) => {\n const isCompleted = step.status === \"completed\";\n const isCurrentPending =\n step.status === \"pending\" &&\n index === steps.findIndex((s) => s.status === \"pending\");\n const isFuturePending = step.status === \"pending\" && !isCurrentPending;\n\n return (\n \n {/* Connector Line */}\n {index < steps.length - 1 && (\n \n )}\n\n {/* Status Icon */}\n \n {isCompleted ? (\n \n ) : isCurrentPending ? (\n \n ) : (\n \n )}\n
\n\n {/* Step Content */}\n
\n \n {step.description}\n
\n {isCurrentPending && (\n \n Processing...\n
\n )}\n \n\n {/* Animated Background for Current Step */}\n {isCurrentPending && (\n \n )}\n \n );\n })}\n \n\n {/* Decorative Elements */}\n \n \n \n \n );\n}\n\n// Enhanced Icons\nfunction CheckIcon() {\n return (\n \n \n \n );\n}\n\nfunction SpinnerIcon() {\n return (\n \n \n \n \n );\n}\n\nfunction ClockIcon({ theme }: { theme?: string }) {\n return (\n \n \n \n \n );\n}\n\nexport default AgenticGenerativeUI;\n", "language": "typescript", "type": "file" }, @@ -4868,273 +4874,267 @@ }, { "name": "README.mdx", - "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", + "content": "# 🚀 Agentic Generative UI Task Executor\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic generative UI** capabilities:\n\n1. **Real-time Status Updates**: The Copilot provides live feedback as it works\n through complex tasks\n2. **Long-running Task Execution**: See how agents can handle extended processes\n with continuous feedback\n3. **Dynamic UI Generation**: The interface updates in real-time to reflect the\n agent's progress\n\n## How to Interact\n\nSimply ask your Copilot to perform any moderately complex task:\n\n- \"Make me a sandwich\"\n- \"Plan a vacation to Japan\"\n- \"Create a weekly workout routine\"\n\nThe Copilot will break down the task into steps and begin \"executing\" them,\nproviding real-time status updates as it progresses.\n\n## ✨ Agentic Generative UI in Action\n\n**What's happening technically:**\n\n- The agent analyzes your request and creates a detailed execution plan\n- Each step is processed sequentially with realistic timing\n- Status updates are streamed to the frontend using CopilotKit's streaming\n capabilities\n- The UI dynamically renders these updates without page refreshes\n- The entire flow is managed by the agent, requiring no manual intervention\n\n**What you'll see in this demo:**\n\n- The Copilot breaks your task into logical steps\n- A status indicator shows the current progress\n- Each step is highlighted as it's being executed\n- Detailed status messages explain what's happening at each moment\n- Upon completion, you receive a summary of the task execution\n\nThis pattern of providing real-time progress for long-running tasks is perfect\nfor scenarios where users benefit from transparency into complex processes -\nfrom data analysis to content creation, system configurations, or multi-stage\nworkflows!\n", "language": "markdown", "type": "file" }, { - "name": "backend_tool_rendering.py", - "content": "\"\"\"Backend tool rendering — a server-side ``@function_tool``.\n\nExercises ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` for a tool the\n*backend* owns and executes (as opposed to :mod:`human_in_the_loop`, where the\nfrontend owns execution). The SDK runs the tool body itself; the translator\njust reports the call and its result as they stream past.\n\nThe dojo's weather card (apps/dojo .../backend_tool_rendering/page.tsx) reads\nthe tool result as a JSON object — temperature/conditions/humidity/\nwind_speed/feels_like — and the argument as `location`, matching every other\nintegration's version of this demo. A plain sentence string or a `city`\nparam renders as a blank/all-zero card, since the frontend has nothing to\nparse. The fixed return value (same for every location) matches how most\nother integrations' versions of this demo work too — the point of the demo\nis the tool-call plumbing, not real weather data.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, function_tool\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n_WEATHER = {\"temperature\": 20, \"conditions\": \"sunny\", \"humidity\": 50, \"wind_speed\": 10, \"feels_like\": 20}\n\n\n@function_tool\ndef get_weather(location: str) -> dict:\n \"\"\"Get the current weather for a location.\"\"\"\n return _WEATHER\n\n\ndef create_backend_tool_agent() -> Agent:\n return Agent(\n name=\"weather_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a helpful weather assistant. Use the get_weather tool \"\n \"whenever the user asks about weather in a specific location.\"\n ),\n tools=[get_weather],\n )\n\n\nagent = OpenAIAgentsAgent(create_backend_tool_agent(), name=\"backend_tool_rendering\")\napp = FastAPI(title=\"Backend tool rendering AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "name": "agentic_generative_ui.py", + "content": "\"\"\"Agentic Generative UI example for Langroid.\n\nThis example demonstrates dynamic UI generation using AG-UI state events.\nThe agent creates plans with steps and updates their status dynamically.\n\"\"\"\nimport json\nimport os\nfrom pathlib import Path\nfrom textwrap import dedent\nfrom typing import Any, Literal, Optional\nfrom dotenv import load_dotenv\n\nenv_path = Path(__file__).parent.parent.parent / '.env'\nload_dotenv(dotenv_path=env_path)\n\nimport langroid as lr\nfrom langroid.agent import ToolMessage, ChatAgent\nfrom langroid.language_models import OpenAIChatModel\nfrom pydantic import BaseModel, Field\nfrom ag_ui.core import EventType, StateSnapshotEvent, StateDeltaEvent\nfrom ag_ui_langroid import LangroidAgent, create_langroid_app\n\nStepStatus = Literal['pending', 'completed']\n\n\nclass Step(BaseModel):\n \"\"\"Represents a step in a plan.\"\"\"\n\n description: str = Field(description='The description of the step')\n status: StepStatus = Field(\n default='pending',\n description='The status of the step (e.g., pending, completed)',\n )\n\n\nclass Plan(BaseModel):\n \"\"\"Represents a plan with multiple steps.\"\"\"\n\n steps: list[Step] = Field(default_factory=list, description='The steps in the plan')\n\n\nclass JSONPatchOp(BaseModel):\n \"\"\"A class representing a JSON Patch operation (RFC 6902).\"\"\"\n\n op: Literal['add', 'remove', 'replace', 'move', 'copy', 'test'] = Field(\n description='The operation to perform: add, remove, replace, move, copy, or test',\n )\n path: str = Field(description='JSON Pointer (RFC 6901) to the target location')\n value: Any = Field(\n default=None,\n description='The value to apply (for add, replace operations)',\n )\n from_: str | None = Field(\n default=None,\n alias='from',\n description='Source path (for move, copy operations)',\n )\n\n\nclass CreatePlanTool(ToolMessage):\n \"\"\"Create a plan with multiple steps.\"\"\"\n request: str = \"create_plan\"\n purpose: str = \"\"\"\n Create a plan with multiple steps.\n Use this when the user asks you to create a plan or break down a task into steps.\n This sets the initial state of the steps.\n \"\"\"\n steps: list[str]\n\n\nclass UpdatePlanStepTool(ToolMessage):\n \"\"\"Update the status or description of a step in the plan.\"\"\"\n request: str = \"update_plan_step\"\n purpose: str = \"\"\"\n Update the status or description of a specific step in the plan.\n Use this to mark steps as completed or update their descriptions.\n The index is 0-based.\n \"\"\"\n index: int\n description: Optional[str] = None\n status: Optional[StepStatus] = None\n\n\n# Configure LLM\nllm_config = lr.language_models.OpenAIGPTConfig(\n chat_model=OpenAIChatModel.GPT4_1_MINI,\n api_key=os.getenv(\"OPENAI_API_KEY\"),\n # Make behavior deterministic for demos and e2e tests\n temperature=0.0,\n)\n\nagent_config = lr.ChatAgentConfig(\n name=\"PlanAssistant\",\n llm=llm_config,\n system_message=dedent(\"\"\"\n You are a helpful assistant that can create plans with multiple steps.\n\n CRITICAL RULES - YOU MUST FOLLOW THESE EXACTLY:\n 1. When the user asks you to create a plan, make a plan, or break down a task into steps, you MUST IMMEDIATELY call the `create_plan` tool. Do NOT respond with text first.\n 2. NEVER say you have \"already created\" a plan unless you have actually called the `create_plan` tool in this conversation.\n 3. NEVER describe steps in your text response - the `create_plan` tool will handle displaying the steps.\n 4. The `create_plan` tool requires a `steps` parameter which is a list of step descriptions as strings.\n 5. After calling `create_plan`, provide a brief summary (1-2 sentences with emojis) of what you did.\n 6. Use `update_plan_step` ONLY when the user explicitly asks to modify an existing plan's steps.\n \n Examples:\n - User: \"give me a plan to make brownies\" → You MUST call create_plan with steps like [\"Gather ingredients\", \"Mix batter\", \"Bake\", etc.]\n - User: \"Go to Mars\" → You MUST call create_plan with steps for a Mars mission\n - User: \"mark step 3 as complete\" → Use update_plan_step to update the status\n \"\"\"),\n use_tools=True,\n use_functions_api=True,\n)\n\n\nclass PlanAssistantAgent(ChatAgent):\n \"\"\"ChatAgent with plan management tool handlers that return AG-UI events.\"\"\"\n \n def __init__(self, config):\n super().__init__(config)\n self._plan_data = None\n self._last_step_update = None\n \n def create_plan(self, msg: CreatePlanTool) -> str:\n \"\"\"\n Handle create_plan tool execution.\n Creates plan and returns result. State events will be handled by handler method.\n Returns string result for Langroid to continue processing.\n Note: Don't include steps in the return value - the handler will emit them via STATE_SNAPSHOT.\n This prevents the frontend from creating a duplicate component from the tool result.\n \"\"\"\n plan = Plan(\n steps=[Step(description=step) for step in msg.steps],\n )\n self._plan_data = plan.model_dump()\n # Return simple confirmation without steps - handler will emit STATE_SNAPSHOT\n # This matches LangGraph's pattern of returning \"Steps executed.\" without the steps\n return json.dumps({\"status\": \"plan_created\", \"steps_count\": len(msg.steps)})\n \n def update_plan_step(\n self, msg: UpdatePlanStepTool\n ) -> str:\n \"\"\"\n Handle update_plan_step tool execution.\n Updates step and returns result. State events will be handled by handler method.\n Returns string result for Langroid to continue processing.\n \"\"\"\n self._last_step_update = {\n \"index\": msg.index,\n \"description\": msg.description,\n \"status\": msg.status\n }\n status_msg = f\"updated step {msg.index}\"\n if msg.status:\n status_msg += f\" to {msg.status}\"\n return json.dumps({\"status\": \"step_updated\", \"index\": msg.index, \"message\": status_msg})\n \n async def _handle_create_plan_result(self, result_data: dict):\n \"\"\"\n Handler for create_plan tool result - emits state events.\n Automatically processes all steps and emits state deltas.\n Uses self._plan_data which was set during create_plan execution.\n \"\"\"\n import asyncio\n import random\n \n # Get steps from _plan_data (set during create_plan) instead of result_data\n # This allows us to return a simple tool result without steps to prevent duplicate components\n if not hasattr(self, \"_plan_data\") or not self._plan_data:\n return\n \n steps = self._plan_data.get(\"steps\", [])\n if not steps:\n return\n \n working_steps = []\n for step in steps:\n if isinstance(step, dict):\n step_dict = dict(step)\n if \"status\" not in step_dict:\n step_dict[\"status\"] = \"pending\"\n working_steps.append(step_dict)\n else:\n working_steps.append({\"description\": str(step), \"status\": \"pending\"})\n \n yield StateSnapshotEvent(\n type=EventType.STATE_SNAPSHOT,\n snapshot={\"steps\": working_steps},\n )\n \n for index, _ in enumerate(working_steps):\n await asyncio.sleep(random.uniform(0.3, 0.8))\n working_steps[index][\"status\"] = \"in_progress\"\n yield StateDeltaEvent(\n type=EventType.STATE_DELTA,\n delta=[\n {\n \"op\": \"replace\",\n \"path\": f\"/steps/{index}/status\",\n \"value\": \"in_progress\",\n }\n ],\n )\n \n await asyncio.sleep(random.uniform(0.4, 1.0))\n working_steps[index][\"status\"] = \"completed\"\n yield StateDeltaEvent(\n type=EventType.STATE_DELTA,\n delta=[\n {\n \"op\": \"replace\",\n \"path\": f\"/steps/{index}/status\",\n \"value\": \"completed\",\n }\n ],\n )\n \n yield StateSnapshotEvent(\n type=EventType.STATE_SNAPSHOT,\n snapshot={\"steps\": working_steps},\n )\n \n async def _handle_update_plan_step_result(self, result_data: dict):\n \"\"\"\n Handler for update_plan_step tool result - emits state delta event.\n \"\"\"\n if not hasattr(self, \"_last_step_update\"):\n return\n \n update = self._last_step_update\n changes = []\n \n if update.get(\"description\") is not None:\n changes.append({\n \"op\": \"replace\",\n \"path\": f\"/steps/{update['index']}/description\",\n \"value\": update[\"description\"],\n })\n if update.get(\"status\") is not None:\n changes.append({\n \"op\": \"replace\",\n \"path\": f\"/steps/{update['index']}/status\",\n \"value\": update[\"status\"],\n })\n \n if changes:\n yield StateDeltaEvent(\n type=EventType.STATE_DELTA,\n delta=changes,\n )\n\n\nchat_agent = PlanAssistantAgent(agent_config)\nchat_agent.enable_message(CreatePlanTool)\nchat_agent.enable_message(UpdatePlanStepTool)\n\ntask = lr.Task(\n chat_agent,\n name=\"PlanAssistant\",\n interactive=False,\n single_round=False,\n)\n\nagui_agent = LangroidAgent(\n agent=task,\n name=\"agentic_generative_ui\",\n description=\"Langroid agent with agentic generative UI support - dynamic plan creation and step updates\",\n)\n\napp = create_langroid_app(agui_agent, \"/\")\n\n", "language": "python", "type": "file" } ], - "openai-agents-python::human_in_the_loop": [ + "langroid::shared_state": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useHumanInTheLoop,\n useConfigureSuggestions,\n CopilotChat,\n CopilotChatConfigurationProvider,\n} from \"@copilotkit/react-core/v2\";\nimport { CopilotKit,\nuseLangGraphInterrupt } from \"@copilotkit/react-core\";\nimport { z } from \"zod\";\nimport { useTheme } from \"next-themes\";\n\ninterface HumanInTheLoopProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst HumanInTheLoop: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\ninterface Step {\n description: string;\n status: \"disabled\" | \"enabled\" | \"executing\";\n}\n\n// Shared UI Components\nconst StepContainer = ({ theme, children }: { theme?: string; children: React.ReactNode }) => (\n
\n \n {children}\n
\n \n);\n\nconst StepHeader = ({\n theme,\n enabledCount,\n totalCount,\n status,\n showStatus = false,\n}: {\n theme?: string;\n enabledCount: number;\n totalCount: number;\n status?: string;\n showStatus?: boolean;\n}) => (\n
\n
\n

\n Select Steps\n

\n
\n
\n {enabledCount}/{totalCount} Selected\n
\n {showStatus && (\n \n {status === \"executing\" ? \"Ready\" : \"Waiting\"}\n
\n )}\n
\n
\n\n \n 0 ? (enabledCount / totalCount) * 100 : 0}%` }}\n />\n \n \n);\n\nconst StepItem = ({\n step,\n theme,\n status,\n onToggle,\n disabled = false,\n}: {\n step: { description: string; status: string };\n theme?: string;\n status?: string;\n onToggle: () => void;\n disabled?: boolean;\n}) => (\n \n \n \n);\n\nconst ActionButton = ({\n variant,\n theme,\n disabled,\n onClick,\n children,\n}: {\n variant: \"primary\" | \"secondary\" | \"success\" | \"danger\";\n theme?: string;\n disabled?: boolean;\n onClick: () => void;\n children: React.ReactNode;\n}) => {\n const baseClasses = \"px-6 py-3 rounded-lg font-semibold transition-all duration-200\";\n const enabledClasses = \"hover:scale-105 shadow-md hover:shadow-lg\";\n const disabledClasses = \"opacity-50 cursor-not-allowed\";\n\n const variantClasses = {\n primary:\n \"bg-gradient-to-r from-purple-500 to-purple-700 hover:from-purple-600 hover:to-purple-800 text-white shadow-lg hover:shadow-xl\",\n secondary:\n theme === \"dark\"\n ? \"bg-slate-700 hover:bg-slate-600 text-white border border-slate-600 hover:border-slate-500\"\n : \"bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-300 hover:border-gray-400\",\n success:\n \"bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white shadow-lg hover:shadow-xl\",\n danger:\n \"bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white shadow-lg hover:shadow-xl\",\n };\n\n return (\n \n {children}\n \n );\n};\n\nconst DecorativeElements = ({\n theme,\n variant = \"default\",\n}: {\n theme?: string;\n variant?: \"default\" | \"success\" | \"danger\";\n}) => (\n <>\n \n \n \n);\nconst InterruptHumanInTheLoop: React.FC<{\n event: { value: { steps: Step[] } };\n resolve: (value: string) => void;\n}> = ({ event, resolve }) => {\n const { theme } = useTheme();\n\n // Parse and initialize steps data\n let initialSteps: Step[] = [];\n if (event.value && event.value.steps && Array.isArray(event.value.steps)) {\n initialSteps = event.value.steps.map((step: any) => ({\n description: typeof step === \"string\" ? step : step.description || \"\",\n status: typeof step === \"object\" && step.status ? step.status : \"enabled\",\n }));\n }\n\n const [localSteps, setLocalSteps] = useState(initialSteps);\n const enabledCount = localSteps.filter((step) => step.status === \"enabled\").length;\n\n const handleStepToggle = (index: number) => {\n setLocalSteps((prevSteps) =>\n prevSteps.map((step, i) =>\n i === index\n ? { ...step, status: step.status === \"enabled\" ? \"disabled\" : \"enabled\" }\n : step,\n ),\n );\n };\n\n const handlePerformSteps = () => {\n const selectedSteps = localSteps\n .filter((step) => step.status === \"enabled\")\n .map((step) => step.description);\n resolve(\"The user selected the following steps: \" + selectedSteps.join(\", \"));\n };\n\n return (\n \n \n\n
\n {localSteps.map((step, index) => (\n handleStepToggle(index)}\n />\n ))}\n
\n\n
\n \n \n Perform Steps\n \n {enabledCount}\n \n \n
\n\n \n
\n );\n};\n\nconst Chat = ({ integrationId }: { integrationId: string }) => {\n return (\n \n \n \n );\n};\n\nconst ChatContent = () => {\n useConfigureSuggestions({\n suggestions: [\n { title: \"Simple plan\", message: \"Please plan a trip to mars in 5 steps.\" },\n { title: \"Complex plan\", message: \"Please plan a pasta dish in 10 steps.\" },\n ],\n available: \"always\",\n });\n\n // Langgraph uses it's own hook to handle human-in-the-loop interactions via langgraph interrupts,\n // This hook won't do anything for other integrations.\n useLangGraphInterrupt({\n \n render: ({ event, resolve }) => ,\n });\n useHumanInTheLoop({\n agentId: \"human_in_the_loop\",\n name: \"generate_task_steps\",\n description: \"Generates a list of steps for the user to perform\",\n parameters: z.object({\n steps: z.array(\n z.object({\n description: z.string(),\n status: z.enum([\"enabled\", \"disabled\", \"executing\"]),\n }),\n ),\n }) ,\n // Note: In v1, `available` was used to disable this for langgraph integrations.\n // In v2, availability is handled at the agent/backend level.\n render: ({ args, respond, status }: any) => {\n return ;\n },\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nconst StepsFeedback = ({ args, respond, status }: { args: any; respond: any; status: any }) => {\n const { theme } = useTheme();\n const [localSteps, setLocalSteps] = useState([]);\n const [accepted, setAccepted] = useState(null);\n\n useEffect(() => {\n if (status === \"executing\" && localSteps.length === 0 && Array.isArray(args?.steps) && args.steps.length > 0) {\n setLocalSteps(args.steps);\n }\n }, [status, args?.steps, localSteps]);\n\n if (!Array.isArray(args?.steps) || args.steps.length === 0) {\n return <>;\n }\n\n const steps = Array.isArray(localSteps) && localSteps.length > 0 ? localSteps : args.steps;\n const enabledCount = steps.filter((step: any) => step.status === \"enabled\").length;\n\n const handleStepToggle = (index: number) => {\n setLocalSteps((prevSteps) =>\n prevSteps.map((step, i) =>\n i === index\n ? { ...step, status: step.status === \"enabled\" ? \"disabled\" : \"enabled\" }\n : step,\n ),\n );\n };\n\n const handleReject = () => {\n if (respond) {\n setAccepted(false);\n respond({ accepted: false });\n }\n };\n\n const handleConfirm = () => {\n if (respond) {\n const confirmedSteps = localSteps.filter((step) => step.status === \"enabled\");\n setAccepted(true);\n respond({ accepted: true, steps: confirmedSteps });\n }\n };\n\n return (\n \n \n\n
\n {steps.map((step: any, index: any) => (\n handleStepToggle(index)}\n disabled={status !== \"executing\"}\n />\n ))}\n
\n\n {/* Action Buttons - Different logic from InterruptHumanInTheLoop */}\n {accepted === null && (\n
\n \n \n Reject\n \n \n \n Confirm\n \n {enabledCount}\n \n \n
\n )}\n\n {/* Result State - Unique to StepsFeedback */}\n {accepted !== null && (\n
\n \n {accepted ? \"✓\" : \"✗\"}\n {accepted ? \"Accepted\" : \"Rejected\"}\n
\n \n )}\n\n \n
\n );\n};\n\nexport default HumanInTheLoop;\n", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n const changedKeysRef = useRef([]);\n\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as any)[key] !== undefined &&\n (agentState.recipe as any)[key] !== null\n ) {\n let agentValue = (agentState.recipe as any)[key];\n const recipeValue = (recipe as any)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as any)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", "language": "typescript", "type": "file" }, + { + "name": "style.css", + "content": "/* Recipe App Styles */\n.app-container {\n min-height: 100vh;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n background-attachment: fixed;\n position: relative;\n overflow: auto;\n}\n\n.recipe-card {\n background-color: rgba(255, 255, 255, 0.97);\n border-radius: 16px;\n box-shadow: 0 15px 30px rgba(0, 0, 0, 0.25), 0 5px 15px rgba(0, 0, 0, 0.15);\n width: 100%;\n max-width: 750px;\n margin: 20px auto;\n padding: 14px 32px;\n position: relative;\n z-index: 1;\n backdrop-filter: blur(5px);\n border: 1px solid rgba(255, 255, 255, 0.3);\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n animation: fadeIn 0.5s ease-out forwards;\n box-sizing: border-box;\n overflow: hidden;\n}\n\n.recipe-card:hover {\n transform: translateY(-5px);\n box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 10px 20px rgba(0, 0, 0, 0.2);\n}\n\n/* Recipe Header */\n.recipe-header {\n margin-bottom: 24px;\n}\n\n.recipe-title-input {\n width: 100%;\n font-size: 24px;\n font-weight: bold;\n border: none;\n outline: none;\n padding: 8px 0;\n margin-bottom: 0px;\n}\n\n.recipe-meta {\n display: flex;\n align-items: center;\n gap: 20px;\n margin-top: 5px;\n margin-bottom: 14px;\n}\n\n.meta-item {\n display: flex;\n align-items: center;\n gap: 8px;\n color: #555;\n}\n\n.meta-icon {\n font-size: 20px;\n color: #777;\n}\n\n.meta-text {\n font-size: 15px;\n}\n\n/* Recipe Meta Selects */\n.meta-item select {\n border: none;\n background: transparent;\n font-size: 15px;\n color: #555;\n cursor: pointer;\n outline: none;\n padding-right: 18px;\n transition: color 0.2s, transform 0.1s;\n font-weight: 500;\n}\n\n.meta-item select:hover,\n.meta-item select:focus {\n color: #FF5722;\n}\n\n.meta-item select:active {\n transform: scale(0.98);\n}\n\n.meta-item select option {\n color: #333;\n background-color: white;\n font-weight: normal;\n padding: 8px;\n}\n\n/* Section Container */\n.section-container {\n margin-bottom: 20px;\n position: relative;\n width: 100%;\n}\n\n.section-title {\n font-size: 20px;\n font-weight: 700;\n margin-bottom: 20px;\n color: #333;\n position: relative;\n display: inline-block;\n}\n\n.section-title:after {\n content: \"\";\n position: absolute;\n bottom: -8px;\n left: 0;\n width: 40px;\n height: 3px;\n background-color: #ff7043;\n border-radius: 3px;\n}\n\n/* Dietary Preferences */\n.dietary-options {\n display: flex;\n flex-wrap: wrap;\n gap: 10px 16px;\n margin-bottom: 16px;\n width: 100%;\n}\n\n.dietary-option {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 14px;\n cursor: pointer;\n margin-bottom: 4px;\n}\n\n.dietary-option input {\n cursor: pointer;\n}\n\n/* Ingredients */\n.ingredients-container {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-bottom: 15px;\n width: 100%;\n box-sizing: border-box;\n}\n\n.ingredient-card {\n display: flex;\n align-items: center;\n background-color: rgba(255, 255, 255, 0.9);\n border-radius: 12px;\n padding: 12px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);\n position: relative;\n transition: all 0.2s ease;\n border: 1px solid rgba(240, 240, 240, 0.8);\n width: calc(33.333% - 7px);\n box-sizing: border-box;\n}\n\n.ingredient-card:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);\n}\n\n.ingredient-card .remove-button {\n position: absolute;\n right: 10px;\n top: 10px;\n background: none;\n border: none;\n color: #ccc;\n font-size: 16px;\n cursor: pointer;\n display: none;\n padding: 0;\n width: 24px;\n height: 24px;\n line-height: 1;\n}\n\n.ingredient-card:hover .remove-button {\n display: block;\n}\n\n.ingredient-icon {\n font-size: 24px;\n margin-right: 12px;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n background-color: #f7f7f7;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.ingredient-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 3px;\n min-width: 0;\n}\n\n.ingredient-name-input,\n.ingredient-amount-input {\n border: none;\n background: transparent;\n outline: none;\n width: 100%;\n padding: 0;\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n}\n\n.ingredient-name-input {\n font-weight: 500;\n font-size: 14px;\n}\n\n.ingredient-amount-input {\n font-size: 13px;\n color: #666;\n}\n\n.ingredient-name-input::placeholder,\n.ingredient-amount-input::placeholder {\n color: #aaa;\n}\n\n.remove-button {\n background: none;\n border: none;\n color: #999;\n font-size: 20px;\n cursor: pointer;\n padding: 0;\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 10px;\n}\n\n.remove-button:hover {\n color: #FF5722;\n}\n\n/* Instructions */\n.instructions-container {\n display: flex;\n flex-direction: column;\n gap: 6px;\n position: relative;\n margin-bottom: 12px;\n width: 100%;\n}\n\n.instruction-item {\n position: relative;\n display: flex;\n width: 100%;\n box-sizing: border-box;\n margin-bottom: 8px;\n align-items: flex-start;\n}\n\n.instruction-number {\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 26px;\n height: 26px;\n background-color: #ff7043;\n color: white;\n border-radius: 50%;\n font-weight: 600;\n flex-shrink: 0;\n box-shadow: 0 2px 4px rgba(255, 112, 67, 0.3);\n z-index: 1;\n font-size: 13px;\n margin-top: 2px;\n}\n\n.instruction-line {\n position: absolute;\n left: 13px; /* Half of the number circle width */\n top: 22px;\n bottom: -18px;\n width: 2px;\n background: linear-gradient(to bottom, #ff7043 60%, rgba(255, 112, 67, 0.4));\n z-index: 0;\n}\n\n.instruction-content {\n background-color: white;\n border-radius: 10px;\n padding: 10px 14px;\n margin-left: 12px;\n flex-grow: 1;\n transition: all 0.2s ease;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);\n border: 1px solid rgba(240, 240, 240, 0.8);\n position: relative;\n width: calc(100% - 38px);\n box-sizing: border-box;\n display: flex;\n align-items: center;\n}\n\n.instruction-content-editing {\n background-color: #fff9f6;\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12), 0 0 0 2px rgba(255, 112, 67, 0.2);\n}\n\n.instruction-content:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);\n}\n\n.instruction-textarea {\n width: 100%;\n background: transparent;\n border: none;\n resize: vertical;\n font-family: inherit;\n font-size: 14px;\n line-height: 1.4;\n min-height: 20px;\n outline: none;\n padding: 0;\n margin: 0;\n}\n\n.instruction-delete-btn {\n position: absolute;\n background: none;\n border: none;\n color: #ccc;\n font-size: 16px;\n cursor: pointer;\n display: none;\n padding: 0;\n width: 20px;\n height: 20px;\n line-height: 1;\n top: 50%;\n transform: translateY(-50%);\n right: 8px;\n}\n\n.instruction-content:hover .instruction-delete-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Action Button */\n.action-container {\n display: flex;\n justify-content: center;\n margin-top: 40px;\n padding-bottom: 20px;\n position: relative;\n}\n\n.improve-button {\n background-color: #ff7043;\n border: none;\n color: white;\n border-radius: 30px;\n font-size: 18px;\n font-weight: 600;\n padding: 14px 28px;\n cursor: pointer;\n transition: all 0.3s ease;\n box-shadow: 0 4px 15px rgba(255, 112, 67, 0.4);\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n position: relative;\n min-width: 180px;\n}\n\n.improve-button:hover {\n background-color: #ff5722;\n transform: translateY(-2px);\n box-shadow: 0 8px 20px rgba(255, 112, 67, 0.5);\n}\n\n.improve-button.loading {\n background-color: #ff7043;\n opacity: 0.8;\n cursor: not-allowed;\n padding-left: 42px; /* Reduced padding to bring text closer to icon */\n padding-right: 22px; /* Balance the button */\n justify-content: flex-start; /* Left align text for better alignment with icon */\n}\n\n.improve-button.loading:after {\n content: \"\"; /* Add space between icon and text */\n display: inline-block;\n width: 8px; /* Width of the space */\n}\n\n.improve-button:before {\n content: \"\";\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83'/%3E%3C/svg%3E\");\n width: 20px; /* Slightly smaller icon */\n height: 20px;\n background-repeat: no-repeat;\n background-size: contain;\n position: absolute;\n left: 16px; /* Slightly adjusted */\n top: 50%;\n transform: translateY(-50%);\n display: none;\n}\n\n.improve-button.loading:before {\n display: block;\n animation: spin 1.5s linear infinite;\n}\n\n@keyframes spin {\n 0% { transform: translateY(-50%) rotate(0deg); }\n 100% { transform: translateY(-50%) rotate(360deg); }\n}\n\n/* Ping Animation */\n.ping-animation {\n position: absolute;\n display: flex;\n width: 12px;\n height: 12px;\n top: 0;\n right: 0;\n}\n\n.ping-circle {\n position: absolute;\n display: inline-flex;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n background-color: #38BDF8;\n opacity: 0.75;\n animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;\n}\n\n.ping-dot {\n position: relative;\n display: inline-flex;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background-color: #0EA5E9;\n}\n\n@keyframes ping {\n 75%, 100% {\n transform: scale(2);\n opacity: 0;\n }\n}\n\n/* Instruction hover effects */\n.instruction-item:hover .instruction-delete-btn {\n display: flex !important;\n}\n\n/* Add some subtle animations */\n@keyframes fadeIn {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* Better center alignment for the recipe card */\n.recipe-card-container {\n display: flex;\n justify-content: center;\n width: 100%;\n position: relative;\n z-index: 1;\n margin: 0 auto;\n box-sizing: border-box;\n}\n\n/* Add Buttons */\n.add-button {\n background-color: transparent;\n color: #FF5722;\n border: 1px dashed #FF5722;\n border-radius: 8px;\n padding: 10px 16px;\n cursor: pointer;\n font-weight: 500;\n display: inline-block;\n font-size: 14px;\n margin-bottom: 0;\n}\n\n.add-step-button {\n background-color: transparent;\n color: #FF5722;\n border: 1px dashed #FF5722;\n border-radius: 6px;\n padding: 6px 12px;\n cursor: pointer;\n font-weight: 500;\n font-size: 13px;\n}\n\n/* Section Headers */\n.section-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 12px;\n}", + "language": "css", + "type": "file" + }, { "name": "README.mdx", - "content": "# 🤝 Human-in-the-Loop Task Planner\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **human-in-the-loop** capabilities:\n\n1. **Collaborative Planning**: The Copilot generates task steps and lets you\n decide which ones to perform\n2. **Interactive Decision Making**: Select or deselect steps to customize the\n execution plan\n3. **Adaptive Responses**: The Copilot adapts its execution based on your\n choices, even handling missing steps\n\n## How to Interact\n\nTry these steps to experience the demo:\n\n1. Ask your Copilot to help with a task, such as:\n\n - \"Make me a sandwich\"\n - \"Plan a weekend trip\"\n - \"Organize a birthday party\"\n - \"Start a garden\"\n\n2. Review the suggested steps provided by your Copilot\n\n3. Select or deselect steps using the checkboxes to customize the plan\n\n - Try removing essential steps to see how the Copilot adapts!\n\n4. Click \"Execute Plan\" to see the outcome based on your selections\n\n## ✨ Human-in-the-Loop Magic in Action\n\n**What's happening technically:**\n\n- The agent analyzes your request and breaks it down into logical steps\n- These steps are presented to you through a dynamic UI component\n- Your selections are captured as user input\n- The agent considers your choices when executing the plan\n- The agent adapts to missing steps with creative problem-solving\n\n**What you'll see in this demo:**\n\n- The Copilot provides a detailed, step-by-step plan for your task\n- You have complete control over which steps to include\n- If you remove essential steps, the Copilot provides entertaining and creative\n workarounds\n- The final execution reflects your choices, showing how human input shapes the\n outcome\n- Each response is tailored to your specific selections\n\nThis human-in-the-loop pattern creates a powerful collaborative experience where\nboth human judgment and AI capabilities work together to achieve better results\nthan either could alone!\n", + "content": "# 🍳 Shared State Recipe Creator\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **shared state** functionality - a powerful\nfeature that enables bidirectional data flow between:\n\n1. **Frontend → Agent**: UI controls update the agent's context in real-time\n2. **Agent → Frontend**: The Copilot's recipe creations instantly update the UI\n components\n\nIt's like having a cooking buddy who not only listens to what you want but also\nupdates your recipe card as you chat - no refresh needed! ✨\n\n## How to Interact\n\nMix and match any of these parameters (or none at all - it's up to you!):\n\n- **Skill Level**: Beginner to expert 👨‍🍳\n- **Cooking Time**: Quick meals or slow cooking ⏱️\n- **Special Preferences**: Dietary needs, flavor profiles, health goals 🥗\n- **Ingredients**: Items you want to include 🧅🥩🍄\n- **Instructions**: Any specific steps\n\nThen chat with your Copilot chef with prompts like:\n\n- \"I'm a beginner cook. Can you make me a quick dinner?\"\n- \"I need something spicy with chicken that takes under 30 minutes!\"\n\n## ✨ Shared State Magic in Action\n\n**What's happening technically:**\n\n- The UI and Copilot agent share the same state object (**Agent State = UI\n State**)\n- Changes from either side automatically update the other\n- Neither side needs to manually request updates from the other\n\n**What you'll see in this demo:**\n\n- Set cooking time to 20 minutes in the UI and watch the Copilot immediately\n respect your time constraint\n- Add ingredients through the UI and see them appear in your recipe\n- When the Copilot suggests new ingredients, watch them automatically appear in\n the UI ingredients list\n- Change your skill level and see how the Copilot adapts its instructions in\n real-time\n\nThis synchronized state creates a seamless experience where the agent always has\nyour current preferences, and any updates to the recipe are instantly reflected\nin both places.\n\nThis shared state pattern can be applied to any application where you want your\nUI and Copilot to work together in perfect harmony!\n", "language": "markdown", "type": "file" }, { - "name": "human_in_the_loop.py", - "content": "\"\"\"Human-in-the-loop — a *frontend*-owned tool, approved before execution.\n\nUnlike :mod:`backend_tool_rendering`, ``generate_task_steps`` has no server\nimplementation — it arrives per-request as an AG-UI client tool\n(``RunAgentInput.tools``), gets wrapped into an SDK ``FunctionTool`` proxy by\n``AGUIToOpenAITranslator.translate_tools()``, and is merged onto this agent by\nthe run loop (see ``server.py``).\n\n``tool_use_behavior=StopAtTools(...)`` is the SDK's built-in for \"the model\nmay only *call* this tool, never execute it\": the run ends the moment the\nmodel emits the call, before the (dead-code) proxy body would ever run. The\nfrontend renders the steps for user approval, then sends the result back as\nan AG-UI ``ToolMessage`` in the *next* request — ordinary multi-turn history,\nno custom pause/resume machinery needed.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_task_steps\"\n\nINSTRUCTIONS = \"\"\"You are a task planning assistant that breaks work into clear, actionable steps.\n\nWhen the user asks for help with a task:\n1. Immediately call the `generate_task_steps` tool with an array of steps.\n Each step is an object: {\"description\": \"...\", \"status\": \"enabled\"}.\n2. Do not restate the steps as plain text — the frontend renders them.\n3. After the call, wait for the user to approve/select steps; do not call\n the tool again until they respond.\n\"\"\"\n\n\ndef create_human_in_the_loop_agent() -> Agent:\n return Agent(\n name=\"task_planner\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n\n\nagent = OpenAIAgentsAgent(create_human_in_the_loop_agent(), name=\"human_in_the_loop\")\napp = FastAPI(title=\"Human in the loop AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "name": "shared_state.py", + "content": "\"\"\"Shared State example for Langroid.\n\nDemonstrates bidirectional state synchronization between agent and UI for recipe collaboration.\n\"\"\"\nimport json\nimport os\nimport logging\nfrom pathlib import Path\nfrom typing import Dict, Any\nfrom dotenv import load_dotenv\n\nenv_path = Path(__file__).parent.parent.parent / '.env'\nload_dotenv(dotenv_path=env_path)\n\nimport langroid as lr\nfrom langroid.agent import ChatAgent, ChatAgentConfig, ToolMessage\nfrom langroid.language_models import OpenAIChatModel, OpenAIGPTConfig\n\nfrom ag_ui_langroid import LangroidAgent, create_langroid_app\nfrom ag_ui_langroid.types import ToolBehavior, LangroidAgentConfig, ToolCallContext\n\nlogger = logging.getLogger(__name__)\n\n\nclass GenerateRecipeTool(ToolMessage):\n \"\"\"Generate or update a recipe.\"\"\"\n request: str = \"generate_recipe\"\n purpose: str = \"\"\"\n Generate or update a recipe using the provided recipe data.\n Always provide the COMPLETE recipe, not just the changes.\n Include all fields: title, skill_level, special_preferences, cooking_time, ingredients, instructions, and changes.\n \"\"\"\n recipe: Dict[str, Any]\n\n\nllm_config = OpenAIGPTConfig(\n chat_model=OpenAIChatModel.GPT4_1_MINI,\n api_key=os.getenv(\"OPENAI_API_KEY\"),\n temperature=0.0,\n)\n\n\nclass RecipeAssistantAgent(ChatAgent):\n \"\"\"ChatAgent with recipe generation capabilities and shared state support.\"\"\"\n\n def __init__(self, config: ChatAgentConfig):\n super().__init__(config)\n self.enable_message(GenerateRecipeTool)\n\n def generate_recipe(self, msg: GenerateRecipeTool) -> str:\n \"\"\"Handle generate_recipe tool execution. State snapshot is emitted via state_from_args.\"\"\"\n return json.dumps({\"status\": \"success\", \"message\": \"Recipe generated successfully\"})\n\n\ndef build_state_context(input_data, user_message: str) -> str:\n \"\"\"Inject current recipe state into prompt.\"\"\"\n state_dict = getattr(input_data, \"state\", None) or {}\n if isinstance(state_dict, dict) and \"recipe\" in state_dict:\n recipe_json = json.dumps(state_dict[\"recipe\"], indent=2)\n return (\n f\"Current recipe state:\\n{recipe_json}\\n\\n\"\n f\"User request: {user_message}\\n\\n\"\n \"Please update the recipe by calling the generate_recipe tool with the COMPLETE updated recipe.\"\n )\n return user_message\n\n\nasync def recipe_state_from_args(context: ToolCallContext):\n \"\"\"Emit recipe snapshot as soon as tool arguments are available.\"\"\"\n try:\n if hasattr(context.tool_input, \"recipe\"):\n recipe_dict = context.tool_input.recipe\n if isinstance(recipe_dict, dict):\n return {\"recipe\": recipe_dict}\n\n if context.args_str:\n args_data = json.loads(context.args_str)\n recipe_dict = args_data.get(\"recipe\")\n if isinstance(recipe_dict, dict):\n return {\"recipe\": recipe_dict}\n\n return None\n except Exception as e:\n logger.warning(f\"Error in recipe_state_from_args: {e}\", exc_info=True)\n return None\n\n\nagent_config = ChatAgentConfig(\n name=\"RecipeAssistant\",\n llm=llm_config,\n system_message=\"\"\"You are a helpful recipe assistant. When asked to improve or modify a recipe:\n\n1. Call the generate_recipe tool ONCE with the COMPLETE updated recipe\n2. Include ALL fields: title, skill_level, special_preferences, cooking_time, ingredients, instructions, and changes\n3. After calling the tool, respond to the user with a brief confirmation of what you changed (1-2 sentences)\n4. Do NOT call the tool multiple times in a row\n5. Keep existing elements that aren't being changed\n6. Do not list the ingredients and instructions in the response, use the tool to display them, unless the user asks for them.\n\nBe creative and helpful!\"\"\",\n use_tools=True,\n use_functions_api=True,\n)\n\nchat_agent = RecipeAssistantAgent(agent_config)\n\ntask = lr.Task(\n chat_agent,\n name=\"RecipeAssistant\",\n interactive=False,\n single_round=False,\n)\n\nshared_state_config = LangroidAgentConfig(\n tool_behaviors={\n \"generate_recipe\": ToolBehavior(\n state_from_args=recipe_state_from_args,\n )\n },\n state_context_builder=build_state_context,\n)\n\nagui_agent = LangroidAgent(\n agent=task,\n name=\"shared_state\",\n description=\"A recipe assistant that collaborates with you to create amazing recipes\",\n config=shared_state_config,\n)\n\napp = create_langroid_app(agui_agent, \"/\")\n", "language": "python", "type": "file" } ], - "openai-agents-python::human_in_the_loop_approval": [ + "watsonx::agentic_chat": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useCopilotKit,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface ApprovalProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface PendingApproval {\n callId: string;\n toolName: string;\n arguments: string;\n}\n\nconst Approval: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"human_in_the_loop_approval\" });\n const { copilotkit } = useCopilotKit();\n const [pending, setPending] = useState(null);\n const [resolvedCallId, setResolvedCallId] = useState(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Refund an order\", message: \"I'd like a refund for ORD-1001.\" },\n { title: \"Refund another order\", message: \"Please refund ORD-1002.\" },\n { title: \"Unknown order\", message: \"Can you refund ORD-9999?\" },\n ],\n available: \"always\",\n consumerAgentId: \"human_in_the_loop_approval\",\n });\n\n React.useEffect(() => {\n const subscription = agent.subscribe({\n onCustomEvent: ({ event }) => {\n if (event.name !== \"approval_request\") return;\n // One CustomEvent can carry several pending calls if the model made\n // more than one needs_approval call in a turn; this demo only ever\n // triggers one, so showing the first is enough here.\n const pendingList = event.value as Array<{\n call_id: string;\n tool_name: string;\n arguments: string;\n }>;\n const first = pendingList[0];\n if (!first) return;\n setResolvedCallId(null);\n setPending({\n callId: first.call_id,\n toolName: first.tool_name,\n arguments: first.arguments,\n });\n },\n onRunFinishedEvent: () => setResolvedCallId(null),\n onRunErrorEvent: () => setResolvedCallId(null),\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n const respond = (approve: boolean) => {\n if (!pending) return;\n setResolvedCallId(pending.callId);\n copilotkit.runAgent({\n agent,\n forwardedProps: {\n approval: { call_id: pending.callId, approve },\n },\n });\n setPending(null);\n };\n\n return (\n
\n {pending && (\n respond(true)} onReject={() => respond(false)} />\n )}\n {!pending && resolvedCallId && (\n
Decision sent — waiting for the agent...
\n )}\n
\n \n
\n
\n );\n};\n\nfunction ApprovalCard({\n pending,\n onApprove,\n onReject,\n}: {\n pending: PendingApproval;\n onApprove: () => void;\n onReject: () => void;\n}) {\n let args: Record = {};\n try {\n args = JSON.parse(pending.arguments);\n } catch {\n // leave args empty — raw string shown below is enough context either way\n }\n\n return (\n \n

Approval needed

\n

\n The agent wants to call {pending.toolName} with:\n

\n
\n        {JSON.stringify(args, null, 2)}\n      
\n
\n \n Reject\n \n \n Approve\n \n
\n \n );\n}\n\nexport default Approval;\n", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useRenderTool,\n useAgentContext,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n\n useAgentContext({\n description: 'Name of the user',\n value: 'Bob'\n });\n\n useFrontendTool({\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients. Only use when asked.\"),\n }) ,\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n return {\n status: \"success\",\n message: `Background changed to ${background}`,\n };\n },\n });\n\n useRenderTool({\n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return
Loading weather...
;\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field reads as\n // undefined and the card renders empty values.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n return (\n
\n Weather in {parsed.city ?? args.location}\n
Temperature: {parsed.temperature}°C
\n
Humidity: {parsed.humidity}%
\n
Wind Speed: {parsed.windSpeed ?? parsed.wind_speed} mph
\n
Conditions: {parsed.conditions}
\n
\n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Change background\",\n message: \"Change the background to something new.\",\n },\n {\n title: \"Generate sonnet\",\n message: \"Write a short sonnet about AI.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n \n
\n \n
\n \n );\n};\n\nexport default AgenticChat;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "---\ntitle: Human in the Loop Approval\ndescription: Pause a backend tool call until a person approves or rejects it.\n---\n\n## Human in the Loop Approval\n\nThis example uses a backend-owned `issue_refund` tool with the OpenAI Agents\nSDK's `needs_approval` option. When the agent asks to issue a refund, the run\npauses and the page shows an approval card.\n\nChoose **Approve** to execute the refund or **Reject** to resume the agent\nwithout executing it. The server keeps the paused SDK state for the thread and\nuses the decision from the next request to continue that same run.\n", + "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", "language": "markdown", "type": "file" }, { - "name": "human_in_the_loop_approval.py", - "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe example's run loop checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom ag_ui_openai_agents.engine import ClientToolPending\nfrom .constants import DEFAULT_MODEL\n\nlogger = logging.getLogger(__name__)\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\ndef resolve_approval(\n store: dict[str, Any],\n thread_id: str,\n forwarded_props: Any,\n) -> tuple[Any, Any, bool]:\n \"\"\"Claim the paused run for this request when a matching decision arrived.\n\n Pops first: whoever gets here wins, so a double-clicked Approve can't\n resume the same run twice. Anything other than a decision that matches a\n pending interruption abandons the paused run and starts a fresh turn —\n the user moved on, and a thread should never be stuck waiting forever.\n\n ``approve`` has to be a real bool. Truthiness would read the string\n \"false\" as approval and run a refund the user declined, so a malformed\n decision is treated as no decision at all.\n\n Args:\n store: thread_id -> paused RunState.\n thread_id: The thread this request belongs to.\n forwarded_props: The request's forwarded_props.\n\n Returns:\n (pending_state, item, approve) to resume, or (None, None, False) to\n run the request fresh.\n \"\"\"\n pending_state = store.pop(thread_id, None)\n if pending_state is None:\n return None, None, False\n\n decision = None\n if isinstance(forwarded_props, dict):\n decision = forwarded_props.get(\"approval\")\n if not isinstance(decision, dict):\n return None, None, False\n\n approve = decision.get(\"approve\")\n if not isinstance(approve, bool):\n return None, None, False\n\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is None:\n return None, None, False\n return pending_state, item, approve\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n pending_state, item, approve = resolve_approval(\n _pending_approvals, body.thread_id, body.forwarded_props\n )\n\n async def stream():\n if item is not None:\n if approve:\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_openai(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n # Collect as we go rather than in one comprehension: whatever streamed\n # before a mid-drain stop still has to reach the client. A client-owned\n # tool ends the run cleanly here; a real failure is kept and re-raised\n # from replay() below, so to_agui sees it and handles it the same way\n # it would on any other route.\n raw_events = []\n stream_error = None\n try:\n async for event in result.stream_events():\n raw_events.append(event)\n except ClientToolPending:\n pass\n except Exception as exc:\n stream_error = exc\n\n end_custom_event = None\n if stream_error is None and result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n if stream_error is not None:\n raise stream_error\n\n try:\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n except Exception:\n # to_agui already sent RUN_ERROR before re-raising; log the real\n # traceback here rather than let it escape the response.\n logger.exception(\"Agent run failed\")\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", - "language": "python", + "name": "index.ts", + "content": "import {\n AbstractAgent,\n type RunAgentInput,\n type BaseEvent,\n type Message,\n type ToolMessage,\n type Tool,\n EventType,\n type RunStartedEvent,\n type RunFinishedEvent,\n type RunErrorEvent,\n type TextMessageStartEvent,\n type TextMessageContentEvent,\n type TextMessageEndEvent,\n type ToolCallStartEvent,\n type ToolCallArgsEvent,\n type ToolCallEndEvent,\n type ToolCallResultEvent,\n type StepStartedEvent,\n type StepFinishedEvent,\n type MessagesSnapshotEvent,\n type RawEvent,\n} from \"@ag-ui/client\";\nimport { Observable } from \"rxjs\";\n\nconst IAM_TOKEN_URL = \"https://iam.cloud.ibm.com/identity/token\";\nconst FETCH_TIMEOUT_MS = 120_000;\nconst MAX_BUFFER_SIZE = 1024 * 1024; // 1MB\n\nexport interface WatsonxAgentConfig {\n region: string;\n instanceId: string;\n agentId: string;\n apiKey?: string;\n bearerToken?: string;\n}\n\nexport class WatsonxAgent extends AbstractAgent {\n private region: string;\n private instanceId: string;\n private watsonxAgentId: string;\n private apiKey?: string;\n private cachedToken?: string;\n private tokenExpiresAt = 0;\n private tokenRefreshPromise?: Promise;\n private activeAbortController?: AbortController;\n private stepInProgress = false;\n\n constructor(config: WatsonxAgentConfig) {\n super({ agentId: config.agentId });\n if (!config.apiKey && !config.bearerToken) {\n throw new Error(\"WatsonxAgent requires either apiKey or bearerToken\");\n }\n this.region = config.region;\n this.instanceId = config.instanceId;\n this.watsonxAgentId = config.agentId;\n this.apiKey = config.apiKey;\n this.cachedToken = config.bearerToken;\n if (config.bearerToken) {\n this.tokenExpiresAt = Date.now() + 55 * 60 * 1000;\n }\n }\n\n private get baseUrl(): string {\n return `https://api.${this.region}.watson-orchestrate.cloud.ibm.com/instances/${this.instanceId}`;\n }\n\n private async getToken(): Promise {\n if (this.cachedToken && Date.now() < this.tokenExpiresAt) {\n return this.cachedToken;\n }\n\n if (!this.apiKey) {\n throw new Error(\n \"watsonx: bearer token expired and no apiKey provided for refresh\",\n );\n }\n\n if (!this.tokenRefreshPromise) {\n this.tokenRefreshPromise = this.refreshToken().finally(() => {\n this.tokenRefreshPromise = undefined;\n });\n }\n return this.tokenRefreshPromise;\n }\n\n private async refreshToken(): Promise {\n const response = await fetch(IAM_TOKEN_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: `grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey=${encodeURIComponent(this.apiKey!)}`,\n });\n\n if (!response.ok) {\n throw new Error(\n `watsonx IAM token exchange failed: HTTP ${response.status}`,\n );\n }\n\n const data = await response.json();\n if (!data.access_token || typeof data.access_token !== \"string\") {\n throw new Error(\"watsonx IAM response missing access_token\");\n }\n if (!data.expiration || typeof data.expiration !== \"number\") {\n throw new Error(\"watsonx IAM response missing expiration\");\n }\n const token: string = data.access_token;\n this.cachedToken = token;\n this.tokenExpiresAt = data.expiration * 1000 - 60_000;\n return token;\n }\n\n run(input: RunAgentInput): Observable {\n return new Observable((subscriber) => {\n const abortController = new AbortController();\n this.activeAbortController = abortController;\n this.stream(input, subscriber, abortController.signal)\n .then(() => subscriber.complete())\n .catch((err) => {\n if (this.stepInProgress) {\n subscriber.next({\n type: EventType.STEP_FINISHED,\n stepName: \"watsonx-orchestrate\",\n } as StepFinishedEvent);\n this.stepInProgress = false;\n }\n const message =\n err instanceof Error\n ? `watsonx request failed: ${err.message.slice(0, 200)}`\n : \"watsonx request failed\";\n const errorEvent: RunErrorEvent = {\n type: EventType.RUN_ERROR,\n message,\n code: \"WATSONX_ERROR\",\n };\n subscriber.next(errorEvent);\n subscriber.complete();\n })\n .finally(() => {\n this.activeAbortController = undefined;\n });\n\n return () => abortController.abort();\n });\n }\n\n private async stream(\n input: RunAgentInput,\n subscriber: import(\"rxjs\").Subscriber,\n signal: AbortSignal,\n ): Promise {\n const { threadId, runId, messages } = input;\n\n const runStarted: RunStartedEvent = {\n type: EventType.RUN_STARTED,\n threadId,\n runId,\n };\n subscriber.next(runStarted);\n\n // Emit TOOL_CALL_RESULT for any tool messages in the input,\n // mirroring langgraph's pattern of surfacing tool results the client sent.\n for (const msg of messages) {\n if (msg.role === \"tool\") {\n const toolMsg = msg as ToolMessage;\n const toolResult: ToolCallResultEvent = {\n type: EventType.TOOL_CALL_RESULT,\n messageId: toolMsg.id,\n toolCallId: toolMsg.toolCallId,\n content: toolMsg.content,\n role: \"tool\",\n };\n subscriber.next(toolResult);\n }\n }\n\n const watsonxMessages = this.mapMessages(messages);\n\n const token = await this.getToken();\n\n const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);\n const combinedSignal = AbortSignal.any([signal, timeoutSignal]);\n\n const { messages: _, stream: _s, tools: _t, ...safeProps } =\n (input.forwardedProps ?? {}) as Record;\n const requestBody: Record = {\n ...safeProps,\n messages: watsonxMessages,\n stream: true,\n };\n\n if (input.tools && input.tools.length > 0) {\n requestBody.tools = input.tools.map((t: Tool) => ({\n type: \"function\",\n function: {\n name: t.name,\n description: t.description || \"\",\n parameters: t.parameters ?? {},\n },\n }));\n }\n\n const stepName = \"watsonx-orchestrate\";\n const stepStarted: StepStartedEvent = {\n type: EventType.STEP_STARTED,\n stepName,\n };\n subscriber.next(stepStarted);\n this.stepInProgress = true;\n\n const response = await fetch(\n `${this.baseUrl}/v1/orchestrate/${this.watsonxAgentId}/chat/completions`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n \"X-IBM-THREAD-ID\": threadId,\n },\n body: JSON.stringify(requestBody),\n signal: combinedSignal,\n },\n );\n\n if (!response.ok || !response.body) {\n throw new Error(`watsonx returned HTTP ${response.status}`);\n }\n\n const reader = response.body\n .pipeThrough(new TextDecoderStream())\n .getReader();\n\n let msgId: string | null = null;\n let msgStarted = false;\n let accumulatedContent = \"\";\n const activeToolCalls = new Map<\n number,\n { id: string; name: string; ended: boolean }\n >();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += value;\n if (buffer.length > MAX_BUFFER_SIZE) {\n throw new Error(\"watsonx SSE buffer exceeded 1MB — aborting\");\n }\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n this.processSSELine(line, subscriber, activeToolCalls, {\n get msgId() {\n return msgId;\n },\n set msgId(v: string | null) {\n msgId = v;\n },\n get msgStarted() {\n return msgStarted;\n },\n set msgStarted(v: boolean) {\n msgStarted = v;\n },\n get accumulatedContent() {\n return accumulatedContent;\n },\n set accumulatedContent(v: string) {\n accumulatedContent = v;\n },\n });\n }\n }\n\n // Process remaining buffer after stream ends\n if (buffer.trim().startsWith(\"data:\")) {\n const trimmed = buffer.trim();\n const data = trimmed.startsWith(\"data: \")\n ? trimmed.slice(6).trim()\n : trimmed.slice(5).trim();\n if (data && data !== \"[DONE]\") {\n try {\n this.processSSELine(trimmed, subscriber, activeToolCalls, {\n get msgId() {\n return msgId;\n },\n set msgId(v: string | null) {\n msgId = v;\n },\n get msgStarted() {\n return msgStarted;\n },\n set msgStarted(v: boolean) {\n msgStarted = v;\n },\n get accumulatedContent() {\n return accumulatedContent;\n },\n set accumulatedContent(v: string) {\n accumulatedContent = v;\n },\n });\n } catch (e) {\n if (!(e instanceof SyntaxError)) throw e;\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n for (const [, tc] of activeToolCalls) {\n if (!tc.ended) {\n const toolEnd: ToolCallEndEvent = {\n type: EventType.TOOL_CALL_END,\n toolCallId: tc.id,\n };\n subscriber.next(toolEnd);\n }\n }\n\n if (msgStarted && msgId) {\n const msgEnd: TextMessageEndEvent = {\n type: EventType.TEXT_MESSAGE_END,\n messageId: msgId,\n };\n subscriber.next(msgEnd);\n }\n\n // Emit STEP_FINISHED now that streaming is complete.\n const stepFinished: StepFinishedEvent = {\n type: EventType.STEP_FINISHED,\n stepName,\n };\n subscriber.next(stepFinished);\n this.stepInProgress = false;\n\n // Emit MESSAGES_SNAPSHOT with the full conversation: input messages\n // plus the assistant's response (if any text was generated).\n const snapshotMessages: Message[] = [...messages];\n if (accumulatedContent && msgId) {\n snapshotMessages.push({\n id: msgId,\n role: \"assistant\",\n content: accumulatedContent,\n });\n }\n const messagesSnapshot: MessagesSnapshotEvent = {\n type: EventType.MESSAGES_SNAPSHOT,\n messages: snapshotMessages,\n };\n subscriber.next(messagesSnapshot);\n\n const runFinished: RunFinishedEvent = {\n type: EventType.RUN_FINISHED,\n threadId,\n runId,\n };\n subscriber.next(runFinished);\n }\n\n private mapMessages(messages: Message[]): Record[] {\n return messages.map((m) => {\n const base: Record = {\n role: m.role,\n content:\n typeof m.content === \"string\" ? m.content : JSON.stringify(m.content),\n };\n if (\"toolCallId\" in m && m.toolCallId) {\n base.tool_call_id = m.toolCallId;\n }\n if (\"toolCalls\" in m && m.toolCalls) {\n base.tool_calls = m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: tc.function.name,\n arguments: tc.function.arguments || \"\",\n },\n }));\n }\n return base;\n });\n }\n\n private processSSELine(\n line: string,\n subscriber: import(\"rxjs\").Subscriber,\n activeToolCalls: Map,\n state: { msgId: string | null; msgStarted: boolean; accumulatedContent: string },\n ): void {\n // Handle both \"data: \" and \"data:\" (without trailing space)\n if (!line.startsWith(\"data:\")) return;\n const data = line.startsWith(\"data: \")\n ? line.slice(6).trim()\n : line.slice(5).trim();\n if (data === \"[DONE]\") return;\n\n let chunk: Record;\n try {\n chunk = JSON.parse(data);\n } catch {\n return;\n }\n\n // Emit a RAW event for every parsed SSE chunk, giving consumers\n // access to platform-specific data for debugging.\n const rawEvent: RawEvent = {\n type: EventType.RAW,\n event: chunk,\n source: \"watsonx\",\n };\n subscriber.next(rawEvent);\n\n const choices = chunk.choices as Array> | undefined;\n const choice = choices?.[0];\n if (!choice) return;\n\n const delta = choice.delta as Record | undefined;\n if (!delta) return;\n\n if (delta.tool_calls) {\n const toolCalls = delta.tool_calls as Array>;\n for (const tc of toolCalls) {\n const idx = (tc.index as number) ?? 0;\n const fn = tc.function as Record | undefined;\n\n if (tc.id && fn?.name) {\n activeToolCalls.set(idx, {\n id: tc.id as string,\n name: fn.name,\n ended: false,\n });\n const toolStart: ToolCallStartEvent = {\n type: EventType.TOOL_CALL_START,\n toolCallId: tc.id as string,\n toolCallName: fn.name,\n };\n subscriber.next(toolStart);\n }\n\n if (fn?.arguments != null && fn.arguments !== \"\") {\n const active = activeToolCalls.get(idx);\n if (active) {\n const toolArgs: ToolCallArgsEvent = {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: active.id,\n delta: fn.arguments,\n };\n subscriber.next(toolArgs);\n }\n }\n }\n // Don't return early — let the finish_reason check below run\n // in case the same chunk also carries finish_reason: \"tool_calls\"\n }\n\n if (delta.content != null && delta.content !== \"\") {\n if (!state.msgStarted) {\n state.msgId = crypto.randomUUID();\n const msgStart: TextMessageStartEvent = {\n type: EventType.TEXT_MESSAGE_START,\n messageId: state.msgId,\n role: \"assistant\",\n };\n subscriber.next(msgStart);\n state.msgStarted = true;\n }\n state.accumulatedContent += delta.content as string;\n const msgContent: TextMessageContentEvent = {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: state.msgId!,\n delta: delta.content as string,\n };\n subscriber.next(msgContent);\n }\n\n const finishReason = (choice as Record).finish_reason;\n if (finishReason === \"stop\" || finishReason === \"tool_calls\") {\n // Close open tool calls for \"tool_calls\" finish reason\n if (finishReason === \"tool_calls\") {\n for (const [, tc] of activeToolCalls) {\n if (!tc.ended) {\n const toolEnd: ToolCallEndEvent = {\n type: EventType.TOOL_CALL_END,\n toolCallId: tc.id,\n };\n subscriber.next(toolEnd);\n tc.ended = true;\n }\n }\n activeToolCalls.clear();\n }\n // Close open text message for \"stop\" finish reason\n if (finishReason === \"stop\" && state.msgStarted && state.msgId) {\n const msgEnd: TextMessageEndEvent = {\n type: EventType.TEXT_MESSAGE_END,\n messageId: state.msgId,\n };\n subscriber.next(msgEnd);\n state.msgStarted = false;\n }\n }\n }\n\n override abortRun(): void {\n this.activeAbortController?.abort();\n this.activeAbortController = undefined;\n }\n\n clone(): WatsonxAgent {\n // Use AbstractAgent.clone() to copy base state (messages, state,\n // description, subscribers, middlewares, pendingInterrupts, etc.)\n const cloned = super.clone() as WatsonxAgent;\n // Overlay watsonx-specific fields\n cloned.region = this.region;\n cloned.instanceId = this.instanceId;\n cloned.watsonxAgentId = this.watsonxAgentId;\n cloned.apiKey = this.apiKey;\n cloned.cachedToken = this.cachedToken;\n cloned.tokenExpiresAt = this.tokenExpiresAt;\n cloned.stepInProgress = false;\n return cloned;\n }\n}\n", + "language": "ts", "type": "file" } ], - "openai-agents-python::tool_based_generative_ui": [ + "watsonx::v1_agentic_chat": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useConfigureSuggestions,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport {\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselNext,\n CarouselPrevious,\n} from \"@/components/ui/carousel\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface ToolBasedGenerativeUIProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ninterface Haiku {\n japanese: string[];\n english: string[];\n image_name: string | null;\n gradient: string;\n}\n\nexport default function ToolBasedGenerativeUI({ params }: ToolBasedGenerativeUIProps) {\n const { integrationId } = React.use(params);\n const { chatDefaultOpen } = useURLParams();\n\n return (\n \n \n \n \n );\n}\n\nfunction SidebarWithSuggestions({ defaultOpen }: { defaultOpen: boolean }) {\n useConfigureSuggestions({\n suggestions: [\n { title: \"Nature Haiku\", message: \"Write me a haiku about nature.\" },\n { title: \"Ocean Haiku\", message: \"Create a haiku about the ocean.\" },\n { title: \"Spring Haiku\", message: \"Generate a haiku about spring.\" },\n ],\n available: \"always\",\n });\n\n return (\n \n );\n}\n\nconst VALID_IMAGE_NAMES = [\n \"Osaka_Castle_Turret_Stone_Wall_Pine_Trees_Daytime.jpg\",\n \"Tokyo_Skyline_Night_Tokyo_Tower_Mount_Fuji_View.jpg\",\n \"Itsukushima_Shrine_Miyajima_Floating_Torii_Gate_Sunset_Long_Exposure.jpg\",\n \"Takachiho_Gorge_Waterfall_River_Lush_Greenery_Japan.jpg\",\n \"Bonsai_Tree_Potted_Japanese_Art_Green_Foliage.jpeg\",\n \"Shirakawa-go_Gassho-zukuri_Thatched_Roof_Village_Aerial_View.jpg\",\n \"Ginkaku-ji_Silver_Pavilion_Kyoto_Japanese_Garden_Pond_Reflection.jpg\",\n \"Senso-ji_Temple_Asakusa_Cherry_Blossoms_Kimono_Umbrella.jpg\",\n \"Cherry_Blossoms_Sakura_Night_View_City_Lights_Japan.jpg\",\n \"Mount_Fuji_Lake_Reflection_Cherry_Blossoms_Sakura_Spring.jpg\",\n];\n\nfunction HaikuDisplay() {\n const [activeIndex, setActiveIndex] = useState(0);\n const [haikus, setHaikus] = useState([\n {\n japanese: [\"仮の句よ\", \"まっさらながら\", \"花を呼ぶ\"],\n english: [\"A placeholder verse—\", \"even in a blank canvas,\", \"it beckons flowers.\"],\n image_name: null,\n gradient: \"\",\n },\n ]);\n\n useFrontendTool(\n {\n agentId: \"tool_based_generative_ui\",\n name: \"generate_haiku\",\n parameters: z.object({\n japanese: z.array(z.string()).describe(\"3 lines of haiku in Japanese\"),\n english: z.array(z.string()).describe(\"3 lines of haiku translated to English\"),\n image_name: z.string().describe(`One relevant image name from: ${VALID_IMAGE_NAMES.join(\", \")}`),\n gradient: z.string().describe(\"CSS Gradient color for the background\"),\n }) ,\n followUp: false,\n handler: async ({ japanese, english, image_name, gradient }: { japanese: string[]; english: string[]; image_name: string; gradient: string }) => {\n const newHaiku: Haiku = {\n japanese: japanese || [],\n english: english || [],\n image_name: image_name || null,\n gradient: gradient || \"\",\n };\n setHaikus((prev) => [\n newHaiku,\n ...prev.filter((h) => h.english[0] !== \"A placeholder verse—\"),\n ]);\n setActiveIndex(0);\n return \"Haiku generated!\";\n },\n render: ({ args }: { args: Partial }) => {\n if (!args.japanese) return <>;\n return ;\n },\n },\n [haikus],\n );\n\n const currentHaiku = haikus[activeIndex];\n\n return (\n
\n
\n \n \n {haikus.map((haiku, index) => (\n \n \n \n ))}\n \n {haikus.length > 1 && (\n <>\n \n \n \n )}\n \n
\n
\n );\n}\n\nfunction HaikuCard({ haiku }: { haiku: Partial }) {\n return (\n \n {/* Decorative background elements */}\n
\n
\n\n {/* Haiku Text */}\n
\n {haiku.japanese?.map((line, index) => (\n \n \n {line}\n

\n \n {haiku.english?.[index]}\n

\n
\n ))}\n
\n\n {/* Image */}\n {haiku.image_name && (\n
\n
\n \n
\n
\n
\n )}\n
\n );\n}\n", + "content": "\"use client\";\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport { CopilotChat } from \"@copilotkit/react-ui\";\nimport \"@copilotkit/react-ui/styles.css\";\n\ninterface V1AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst V1AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n
\n
\n \n
\n
\n \n );\n};\n\nexport default V1AgenticChat;\n", "language": "typescript", "type": "file" }, - { - "name": "style.css", - "content": ".page-background {\n /* Darker gradient background */\n background: linear-gradient(170deg, #e9ecef 0%, #ced4da 100%);\n}\n\n@keyframes fade-scale-in {\n from {\n opacity: 0;\n transform: translateY(10px) scale(0.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n/* Updated card entry animation */\n@keyframes pop-in {\n 0% {\n opacity: 0;\n transform: translateY(15px) scale(0.95);\n }\n 70% {\n opacity: 1;\n transform: translateY(-2px) scale(1.02);\n }\n 100% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n/* Animation for subtle background gradient movement */\n@keyframes animated-gradient {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n\n/* Animation for flash effect on apply */\n@keyframes flash-border-glow {\n 0% {\n /* Start slightly intensified */\n border-top-color: #ff5b4a !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 25px rgba(255, 91, 74, 0.5);\n }\n 50% {\n /* Peak intensity */\n border-top-color: #ff4733 !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 35px rgba(255, 71, 51, 0.7);\n }\n 100% {\n /* Return to default state appearance */\n border-top-color: #ff6f61 !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 10px rgba(255, 111, 97, 0.15);\n }\n}\n\n/* Existing animation for haiku lines */\n@keyframes fade-slide-in {\n from {\n opacity: 0;\n transform: translateX(-15px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n.animated-fade-in {\n /* Use the new pop-in animation */\n animation: pop-in 0.6s ease-out forwards;\n}\n\n.haiku-card {\n /* Subtle animated gradient background */\n background: linear-gradient(120deg, #ffffff 0%, #fdfdfd 50%, #ffffff 100%);\n background-size: 200% 200%;\n animation: animated-gradient 10s ease infinite;\n\n /* === Explicit Border Override Attempt === */\n /* 1. Set the default grey border for all sides */\n border: 1px solid #dee2e6;\n\n /* 2. Explicitly override the top border immediately after */\n border-top: 10px solid #ff6f61 !important; /* Orange top - Added !important */\n /* === End Explicit Border Override Attempt === */\n\n padding: 2.5rem 3rem;\n border-radius: 20px;\n\n /* Default glow intensity */\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 15px rgba(255, 111, 97, 0.25);\n text-align: left;\n max-width: 745px;\n margin: 3rem auto;\n min-width: 600px;\n\n /* Transition */\n transition: transform 0.35s ease, box-shadow 0.35s ease, border-top-width 0.35s ease, border-top-color 0.35s ease;\n}\n\n.haiku-card:hover {\n transform: translateY(-8px) scale(1.03);\n /* Enhanced shadow + Glow */\n box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 25px rgba(255, 91, 74, 0.5);\n /* Modify only top border properties */\n border-top-width: 14px !important; /* Added !important */\n border-top-color: #ff5b4a !important; /* Added !important */\n}\n\n.haiku-card .flex {\n margin-bottom: 1.5rem;\n}\n\n.haiku-card .flex.haiku-line { /* Target the lines specifically */\n margin-bottom: 1.5rem;\n opacity: 0; /* Start hidden for animation */\n animation: fade-slide-in 0.5s ease-out forwards;\n /* animation-delay is set inline in page.tsx */\n}\n\n/* Remove previous explicit color overrides - rely on Tailwind */\n/* .haiku-card p.text-4xl {\n color: #212529;\n}\n\n.haiku-card p.text-base {\n color: #495057;\n} */\n\n.haiku-card.applied-flash {\n /* Apply the flash animation once */\n /* Note: animation itself has !important on border-top-color */\n animation: flash-border-glow 0.6s ease-out forwards;\n}\n\n/* Styling for images within the main haiku card */\n.haiku-card-image {\n width: 9.5rem; /* Increased size (approx w-48) */\n height: 9.5rem; /* Increased size (approx h-48) */\n object-fit: cover;\n border-radius: 1.5rem; /* rounded-xl */\n border: 1px solid #e5e7eb;\n /* Enhanced shadow with subtle orange hint */\n box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1),\n 0 3px 6px rgba(0, 0, 0, 0.08),\n 0 0 10px rgba(255, 111, 97, 0.2);\n /* Inherit animation delay from inline style */\n animation-name: fadeIn;\n animation-duration: 0.5s;\n animation-fill-mode: both;\n}\n\n/* Styling for images within the suggestion card */\n.suggestion-card-image {\n width: 6.5rem; /* Increased slightly (w-20) */\n height: 6.5rem; /* Increased slightly (h-20) */\n object-fit: cover;\n border-radius: 1rem; /* Equivalent to rounded-md */\n border: 1px solid #d1d5db; /* Equivalent to border (using Tailwind gray-300) */\n margin-top: 0.5rem;\n /* Added shadow for suggestion images */\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1),\n 0 2px 4px rgba(0, 0, 0, 0.06);\n transition: all 0.2s ease-in-out; /* Added for smooth deselection */\n}\n\n/* Styling for the focused suggestion card image */\n.suggestion-card-image-focus {\n width: 6.5rem;\n height: 6.5rem;\n object-fit: cover;\n border-radius: 1rem;\n margin-top: 0.5rem;\n /* Highlight styles */\n border: 2px solid #ff6f61; /* Thicker, themed border */\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), /* Base shadow for depth */\n 0 0 12px rgba(255, 111, 97, 0.6); /* Orange glow */\n transform: scale(1.05); /* Slightly scale up */\n transition: all 0.2s ease-in-out; /* Smooth transition for focus */\n}\n\n/* Styling for the suggestion card container in the sidebar */\n.suggestion-card {\n border: 1px solid #dee2e6; /* Same default border as haiku-card */\n border-top: 10px solid #ff6f61; /* Same orange top border */\n border-radius: 0.375rem; /* Default rounded-md */\n /* Note: background-color is set by Tailwind bg-gray-100 */\n /* Other styles like padding, margin, flex are handled by Tailwind */\n}\n\n.suggestion-image-container {\n display: flex;\n gap: 1rem;\n justify-content: space-between;\n width: 100%;\n height: 6.5rem;\n}\n\n/* Mobile responsive styles - matches useMobileView hook breakpoint */\n@media (max-width: 767px) {\n .haiku-card {\n padding: 1rem 1.5rem; /* Reduced from 2.5rem 3rem */\n min-width: auto; /* Remove min-width constraint */\n max-width: 100%; /* Full width on mobile */\n margin: 1rem auto; /* Reduced margin */\n }\n\n .haiku-card-image {\n width: 5.625rem; /* 90px - smaller on mobile */\n height: 5.625rem; /* 90px - smaller on mobile */\n }\n\n .suggestion-card-image {\n width: 5rem; /* Slightly smaller on mobile */\n height: 5rem; /* Slightly smaller on mobile */\n }\n\n .suggestion-card-image-focus {\n width: 5rem; /* Slightly smaller on mobile */\n height: 5rem; /* Slightly smaller on mobile */\n }\n}\n", - "language": "css", - "type": "file" - }, { "name": "README.mdx", - "content": "# 🪶 Tool-Based Generative UI Haiku Creator\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **tool-based generative UI** capabilities:\n\n1. **Frontend Rendering of Tool Calls**: Backend tool calls are automatically\n rendered in the UI\n2. **Dynamic UI Generation**: The UI updates in real-time as the agent generates\n content\n3. **Elegant Content Presentation**: Complex structured data (haikus) are\n beautifully displayed\n\n## How to Interact\n\nChat with your Copilot and ask for haikus about different topics:\n\n- \"Create a haiku about nature\"\n- \"Write a haiku about technology\"\n- \"Generate a haiku about the changing seasons\"\n- \"Make a humorous haiku about programming\"\n\nEach request will trigger the agent to generate a haiku and display it in a\nvisually appealing card format in the UI.\n\n## ✨ Tool-Based Generative UI in Action\n\n**What's happening technically:**\n\n- The agent processes your request and determines it should create a haiku\n- It calls a backend tool that returns structured haiku data\n- CopilotKit automatically renders this tool call in the frontend\n- The rendering is handled by the registered tool component in your React app\n- No manual state management is required to display the results\n\n**What you'll see in this demo:**\n\n- As you request a haiku, a beautifully formatted card appears in the UI\n- The haiku follows the traditional 5-7-5 syllable structure\n- Each haiku is presented with consistent styling\n- Multiple haikus can be generated in sequence\n- The UI adapts to display each new piece of content\n\nThis pattern of tool-based generative UI can be extended to create any kind of\ndynamic content - from data visualizations to interactive components, all driven\nby your Copilot's tool calls!\n", + "content": "# 🤖 V1 Agentic Chat\n\n## What This Demo Shows\n\nThis demo verifies **CopilotKit v1 API compatibility**. It uses the original v1\ncomponents (`CopilotKit` provider and `CopilotChat`) to ensure that v1 APIs\ncontinue to work correctly against the current runtime.\n\n1. **V1 Provider**: Uses `CopilotKit` from `@copilotkit/react-core` with the\n `agent` prop for agent selection\n2. **V1 Chat UI**: Uses `CopilotChat` from `@copilotkit/react-ui` with v1\n styling\n3. **Same Backend**: Connects to the same runtime endpoint as v2, validating\n backward compatibility\n\n## How to Interact\n\nThis is a standard chat interface — type a message and the agent will respond\nconversationally, just like the v2 agentic chat demo.\n\n## ✨ V1 Compatibility\n\n**What's happening technically:**\n\n- The v1 `CopilotKit` provider connects to the same `/api/copilotkit/[integration]` endpoint\n- The v1 chat UI renders with v1 CSS classes (`.copilotKitInput`, `.copilotKitAssistantMessage`, etc.)\n- The agent selected via the `agent` prop maps to the same `agentic_chat` backend agent\n- This ensures that applications built with v1 APIs continue to function after runtime upgrades\n", "language": "markdown", "type": "file" - }, - { - "name": "tool_based_generative_ui.py", - "content": "\"\"\"Tool-based generative UI — frontend tool renders the content.\n\nLike :mod:`human_in_the_loop`, the tool (``generate_haiku``) is *client*-owned:\nit arrives per-request in ``RunAgentInput.tools``, gets wrapped into an SDK\n``FunctionTool`` proxy, and ``StopAtTools`` ends the run the moment the model\ncalls it. The difference is intent: here the tool call *is* the deliverable —\nthe frontend renders the haiku card from the streamed ``TOOL_CALL_ARGS``,\nno approval round-trip expected.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_haiku\"\n\nINSTRUCTIONS = \"\"\"You are a creative writing assistant that renders haikus with a UI component.\n\nWhen the user asks for a haiku:\n1. Immediately call the `generate_haiku` tool with the haiku data\n (Japanese lines, English lines, and any other fields the tool declares).\n2. Do NOT write the haiku as plain text — the frontend renders it.\n\nFor non-creative requests, respond normally without the tool.\n\"\"\"\n\n\ndef create_tool_based_generative_ui_agent() -> Agent:\n return Agent(\n name=\"haiku_assistant\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n\n\nagent = OpenAIAgentsAgent(\n create_tool_based_generative_ui_agent(), name=\"tool_based_generative_ui\"\n)\napp = FastAPI(title=\"Tool-based generative UI AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", - "language": "python", - "type": "file" } ], - "openai-agents-python::subagents": [ + "openai-agents-python::ag_ui_docs_copilot": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useConfigureSuggestions, useRenderTool } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { z } from \"zod\";\n\ninterface SubagentsProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ntype Role = \"research\" | \"writer\" | \"critic\";\ntype Status = \"inProgress\" | \"executing\" | \"complete\";\n\nconst ROLES: { role: Role; label: string; emoji: string }[] = [\n { role: \"research\", label: \"Researcher\", emoji: \"🔎\" },\n { role: \"writer\", label: \"Writer\", emoji: \"✍️\" },\n { role: \"critic\", label: \"Critic\", emoji: \"🧐\" },\n];\n\ninterface DelegationEntry {\n id: string;\n role: Role;\n status: Status;\n text: string;\n}\n\nconst Subagents: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst SubagentsView = () => {\n const [active, setActive] = useState>({\n research: null,\n writer: null,\n critic: null,\n });\n const [log, setLog] = useState([]);\n\n // Called by each DelegationTracker instance as its tool call's status\n // changes. Same shared state feeds both the role chips (who's working\n // right now) and the log (what happened, in order).\n const track = useCallback((entry: DelegationEntry) => {\n setActive((prev) => ({\n ...prev,\n [entry.role]: entry.status === \"complete\" ? null : entry.status,\n }));\n setLog((prev) => {\n const idx = prev.findIndex((e) => e.id === entry.id);\n if (idx === -1) return [...prev, entry];\n const next = [...prev];\n next[idx] = entry;\n return next;\n });\n }, []);\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"History of AI\",\n message: \"Write a short article about the history of AI\",\n },\n {\n title: \"History of programming languages\",\n message: \"Write a short piece about the history of programming languages\",\n },\n ],\n available: \"always\",\n });\n\n useRenderTool({\n name: \"research_topic\",\n agentId: \"subagents\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"write_prose\",\n agentId: \"subagents\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"critique_draft\",\n agentId: \"subagents\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n return (\n
\n
\n

Supervisor's team

\n
\n {ROLES.map(({ role, label, emoji }) => {\n const status = active[role];\n return (\n \n {emoji}\n {label}\n {status && (\n \n {status === \"executing\" ? \"working\" : \"starting\"}\n \n )}\n
\n );\n })}\n
\n\n

\n Delegation log\n

\n
\n {log.length === 0 && (\n
No delegations yet.
\n )}\n {log.map((entry) => {\n const roleInfo = ROLES.find((r) => r.role === entry.role);\n return (\n \n
\n \n {roleInfo?.emoji} {roleInfo?.label}\n \n \n {entry.status}\n \n
\n {entry.text && (\n
\n {entry.text}\n
\n )}\n
\n );\n })}\n
\n
\n\n
\n
\n \n
\n
\n \n );\n};\n\n// Each tool call renders its own instance of this. `render` is invoked as a\n// real component (not a plain function), so effects are legal here — this\n// is what reports status upstream into the shared chip/log state on every\n// inProgress → executing → complete transition, in addition to rendering\n// its own inline card in the chat transcript.\nfunction DelegationTracker({\n role,\n toolCallId,\n status,\n preview,\n result,\n onUpdate,\n}: {\n role: Role;\n toolCallId: string;\n status: Status;\n preview?: string;\n result?: string;\n onUpdate: (entry: DelegationEntry) => void;\n}) {\n const text = status === \"complete\" ? (result ?? \"\") : (preview ?? \"\");\n\n useEffect(() => {\n onUpdate({ id: toolCallId, role, status, text });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [toolCallId, role, status, text]);\n\n const roleInfo = ROLES.find((r) => r.role === role)!;\n const label =\n status === \"complete\"\n ? `${roleInfo.label} finished`\n : status === \"executing\"\n ? `${roleInfo.label} working…`\n : `Calling ${roleInfo.label.toLowerCase()}…`;\n\n return (\n
\n {roleInfo.emoji} {label}\n
\n );\n}\n\nexport default Subagents;\n", + "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"read_ag_ui_openai_agents_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ heading: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n useRenderTool({\n name: \"read_ag_ui_protocol_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ heading: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents (Python) streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents (Python) streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n source,\n status,\n heading,\n result,\n}: {\n source: \"AG-UI OpenAI Agents\" | \"AG-UI Protocol\";\n status: \"inProgress\" | \"executing\" | \"complete\";\n heading?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const missed = complete && !!result && result.startsWith(\"No section matches\");\n\n return (\n
\n \n {missed ? \"!\" : complete ? \"✓\" : \"📖\"}\n \n
\n
\n \n {source} docs\n \n {!complete && (\n \n \n \n •\n \n \n •\n \n \n )}\n
\n
\n {heading\n ? `${complete ? \"Read\" : \"Reading\"} \"${heading}\"`\n : complete\n ? \"Section read\"\n : \"Opening the guide\"}\n
\n
\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# 🧭 Subagents\n\n## What This Demo Shows\n\nA supervisor agent stays in charge of the conversation and calls specialist\nagents as tools (`Agent.as_tool()`), possibly several in one turn, then\nsynthesizes their outputs itself — a call-and-return delegation, not a\nhandoff (control never transfers away from the supervisor). Same shape as\nCopilotKit's own LangGraph \"subagents\" showcase demo (a supervisor calling\nchild agents as tools).\n\nA sidebar tracks the team live: role chips light up while a specialist is\nworking, and a delegation log lists each call with its status and a\npreview of the input/output.\n\n## How to Interact\n\n- \"Write a short piece about the history of coffee\" (calls research,\n writer, and critic specialists in sequence, then produces a final draft)\n- \"What is 2 + 2?\" (answered directly, no team involved)\n\n## Technical Details\n\n- Each specialist invocation is an ordinary `TOOL_CALL_START/ARGS/END` +\n `TOOL_CALL_RESULT` sequence\n- The nested agents' own model turns stay internal to the SDK and are not\n streamed as separate messages — the client sees one coherent transcript\n- The sidebar is built from three `useRenderTool` renderers (one per\n specialist tool name), each reporting its status into shared page state\n via a small tracker component\n", + "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nOpenAI Agents (Python).\n\nOne **Copilot** agent handles normal conversation directly, and for AG-UI or\nOpenAI Agents (Python) questions call one of two tools, `read_ag_ui_protocol_docs`\nand `read_ag_ui_openai_agents_docs`, to read a single section of the relevant\nlocal README by heading.\n\n## What it demonstrates\n\n```text\nCopilot → read_ag_ui_protocol_docs / read_ag_ui_openai_agents_docs (local READMEs, one section at a time)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- Table-of-contents instructions plus an on-demand section-read tool, instead\n of loading a whole README into the prompt\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded by section\n\nThe integration README is small enough for this focused demo, so its heading\nlist sits in the Copilot's instructions and each tool call reads back one\nmatching section. This keeps the example deterministic, cheap, and fast to\nstream — an earlier version routed each question through a second\nsub-agent, which cost extra model round trips before the first token showed\nup. A production assistant with many or large documents should replace this\nwith its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask for a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", "language": "markdown", "type": "file" }, { - "name": "subagents.py", - "content": "\"\"\"Subagents — multi-agent via the SDK's agents-as-tools pattern.\n\nUnlike a handoff (control *transfers* to the specialist, triage agent exits\nthe conversation), the supervisor here stays in charge and *calls*\nspecialists as tools (``Agent.as_tool()``), possibly several in one turn,\nthen synthesizes their outputs itself — a call-and-return delegation, not a\nhandoff. Same shape as the LangGraph \"subagents\" showcase demo (a supervisor\ncalling child agents as tools and getting results back), just built with the\nSDK's own ``as_tool()`` instead of a routing graph.\n\nOn the AG-UI side each specialist invocation is an ordinary\n``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested\nagent's own model turns stay internal to the SDK and are not streamed as\nseparate messages, so the client sees one coherent supervisor transcript.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\nresearch_agent = Agent(\n name=\"research_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You research a topic and return the key facts as a short bullet \"\n \"list. Facts only — no fluff, no conclusions.\"\n ),\n)\n\nwriter_agent = Agent(\n name=\"writer_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You turn bullet-point facts into short, engaging prose. One tight \"\n \"paragraph unless asked otherwise.\"\n ),\n)\n\ncritic_agent = Agent(\n name=\"critic_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You review a draft and return concrete improvement suggestions as a \"\n \"numbered list. Max 3 suggestions, be specific.\"\n ),\n)\n\n\ndef create_subagents_agent() -> Agent:\n return Agent(\n name=\"orchestrator\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You orchestrate a small content team. For writing requests: \"\n \"call research_topic for the facts, then write_prose to draft, \"\n \"then critique_draft to review, then produce the final version \"\n \"yourself incorporating the critique. For simple questions, \"\n \"answer directly without the team.\"\n ),\n tools=[\n research_agent.as_tool(\n tool_name=\"research_topic\",\n tool_description=\"Research a topic and return key facts as bullets.\",\n ),\n writer_agent.as_tool(\n tool_name=\"write_prose\",\n tool_description=\"Turn bullet-point facts into short prose.\",\n ),\n critic_agent.as_tool(\n tool_name=\"critique_draft\",\n tool_description=\"Review a draft and suggest up to 3 improvements.\",\n ),\n ],\n )\n\n\nagent = OpenAIAgentsAgent(create_subagents_agent(), name=\"subagents\")\napp = FastAPI(title=\"Subagents AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "name": "ag_ui_docs_copilot.py", + "content": "\"\"\"AG-UI documentation assistant that reads local README sections on demand.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom importlib.metadata import metadata\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n_HEADING_RE = re.compile(r\"^#{1,6}\\s+(.+)$\")\n\n\ndef _load_distribution_readme(distribution: str) -> str:\n \"\"\"Load the installed package README from its distribution metadata.\"\"\"\n document = metadata(distribution).get_payload()\n if not isinstance(document, str) or not document.strip():\n raise RuntimeError(f\"{distribution} has no README in its package metadata\")\n return document\n\n\nclass MarkdownSections:\n \"\"\"A Markdown document split by heading, read one section at a time.\n\n The Copilot sees only the heading list in its instructions and fetches\n the sections it needs on demand, so the full document never sits in a\n prompt.\n \"\"\"\n\n def __init__(self, document: str) -> None:\n sections: dict[str, list[str]] = {}\n heading = \"Overview\"\n for line in document.splitlines():\n match = _HEADING_RE.match(line)\n if match:\n heading = match.group(1).strip()\n sections.setdefault(heading, []).append(line)\n self._sections = {\n name: content\n for name, lines in sections.items()\n if (content := \"\\n\".join(lines).strip())\n }\n\n @property\n def headings(self) -> str:\n \"\"\"The table of contents: one heading per line.\"\"\"\n return \"\\n\".join(f\"- {name}\" for name in self._sections)\n\n def read(self, heading: str) -> str:\n \"\"\"Return one section by heading (case-insensitive, substring ok).\"\"\"\n wanted = heading.strip().lstrip(\"#\").strip().lower()\n for name, content in self._sections.items():\n if name.lower() == wanted:\n return content\n for name, content in self._sections.items():\n if wanted in name.lower():\n return content\n return f\"No section matches {heading!r}. Available headings:\\n{self.headings}\"\n\n\n# ag-ui-protocol docs: the core Python SDK README.\nAG_UI_PROTOCOL_DOCS = _load_distribution_readme(\"ag-ui-protocol\")\n_AG_UI_PROTOCOL_SECTIONS = MarkdownSections(AG_UI_PROTOCOL_DOCS)\n\n\n@function_tool\ndef read_ag_ui_protocol_docs(heading: str) -> str:\n \"\"\"Read one AG-UI Protocol Python documentation section by its heading.\n\n Args:\n heading: A heading from the \"AG-UI Protocol docs\" table of contents\n in your instructions.\n \"\"\"\n return _AG_UI_PROTOCOL_SECTIONS.read(heading)\n\n\n# ag-ui-openai-agents docs: this integration's README.\nAG_UI_OPENAI_AGENTS_DOCS = _load_distribution_readme(\"ag-ui-openai-agents\")\n_AG_UI_OPENAI_AGENTS_SECTIONS = MarkdownSections(AG_UI_OPENAI_AGENTS_DOCS)\n\n\n@function_tool\ndef read_ag_ui_openai_agents_docs(heading: str) -> str:\n \"\"\"Read one AG-UI OpenAI Agents integration documentation section.\n\n Args:\n heading: A heading from the \"AG-UI OpenAI Agents docs\" table of\n contents in your instructions.\n \"\"\"\n return _AG_UI_OPENAI_AGENTS_SECTIONS.read(heading)\n\n\ncopilot_instructions = f\"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question about AG-UI, OpenAI Agents (Python), translator APIs, FastAPI\nendpoints, streaming, tools, client tools, state, lifecycle events, errors,\ntests, or Python implementation, pick the most relevant heading from the\nmatching table of contents below and call the matching tool before answering.\nUse read_ag_ui_openai_agents_docs for OpenAI Agents (Python) integration questions\nand read_ag_ui_protocol_docs for ag_ui.core protocol types and EventEncoder\nquestions. Treat the returned section as your only source of truth. Read\nanother section if the first one is not enough.\n\nUse only the part of the section that answers the user's question. Do not\ninvent behavior or claim details the documentation did not provide. For code\nrequests, provide only the smallest complete, production-readable Python\nsnippet needed, followed by a short explanation. Use documented APIs only.\n\nAG-UI Protocol docs table of contents:\n{_AG_UI_PROTOCOL_SECTIONS.headings}\n\nAG-UI OpenAI Agents docs table of contents:\n{_AG_UI_OPENAI_AGENTS_SECTIONS.headings}\n\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[read_ag_ui_protocol_docs, read_ag_ui_openai_agents_docs],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", "language": "python", "type": "file" } ], - "openai-agents-python::custom_lifecycle_events": [ + "openai-agents-python::agentic_chat": [ { "name": "page.tsx", - "content": "\"use client\";\n\nimport React, {\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface UsageInfo {\n tokens: number;\n costUsd: number;\n}\n\ninterface RunUsage {\n input?: UsageInfo;\n output?: UsageInfo;\n}\n\nconst runUsage = new Map();\nconst usageListeners = new Set<() => void>();\n\nfunction setRunUsage(runId: string, update: RunUsage) {\n runUsage.set(runId, { ...runUsage.get(runId), ...update });\n usageListeners.forEach((listener) => listener());\n}\n\nfunction subscribeRunUsage(listener: () => void) {\n usageListeners.add(listener);\n return () => usageListeners.delete(listener);\n}\n\nfunction getRunUsage(runId?: string) {\n return runId ? runUsage.get(runId) : undefined;\n}\n\nfunction UsageLabel({\n message,\n position,\n runId,\n}: {\n message: { role: string };\n position: \"before\" | \"after\";\n runId?: string;\n}) {\n const usage = useSyncExternalStore(\n subscribeRunUsage,\n () => getRunUsage(runId),\n () => undefined,\n );\n\n if (\n message.role !== \"assistant\" ||\n position !== \"after\" ||\n (!usage?.input && !usage?.output)\n ) {\n return null;\n }\n\n return (\n \n {usage.input && (\n \n ⬇️ {usage.input.tokens} tokens · ${usage.input.costUsd.toFixed(6)}\n \n )}\n {usage.input && usage.output && }\n {usage.output && (\n \n ⬆️ {usage.output.tokens} tokens · ${usage.output.costUsd.toFixed(6)}\n \n )}\n \n );\n}\n\nconst CustomLifecycleEvents: React.FC = ({\n params,\n}) => {\n const { integrationId } = React.use(params);\n const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const currentRunId = useRef(undefined);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n {\n title: \"Tell me a fact about AI\",\n message: \"Tell me one interesting fact about AI.\",\n },\n ],\n available: \"before-first-message\",\n consumerAgentId: \"custom_lifecycle_events\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onRunStartedEvent: ({ event }) => {\n currentRunId.current = event.runId;\n runUsage.delete(event.runId);\n usageListeners.forEach((listener) => listener());\n },\n onCustomEvent: ({ event, input }) => {\n // The subscriber context carries the run input for every event. Use\n // it directly so usage events are not lost if RUN_STARTED and CUSTOM\n // are delivered before the lifecycle callback updates the ref.\n const runId = input.runId ?? currentRunId.current;\n if (!runId) return;\n\n const value = event.value as { tokens: number; cost_usd: number };\n if (event.name === \"input_usage\") {\n setRunUsage(runId, {\n input: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n if (event.name === \"output_usage\") {\n setRunUsage(runId, {\n output: { tokens: value.tokens, costUsd: value.cost_usd },\n });\n }\n },\n onRunFinishedEvent: () => {\n currentRunId.current = undefined;\n },\n onRunErrorEvent: () => {\n currentRunId.current = undefined;\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default CustomLifecycleEvents;\n", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useRenderTool,\n useAgentContext,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n\n useAgentContext({\n description: 'Name of the user',\n value: 'Bob'\n });\n\n useFrontendTool({\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients. Only use when asked.\"),\n }) ,\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n return {\n status: \"success\",\n message: `Background changed to ${background}`,\n };\n },\n });\n\n useRenderTool({\n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return
Loading weather...
;\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field reads as\n // undefined and the card renders empty values.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n return (\n
\n Weather in {parsed.city ?? args.location}\n
Temperature: {parsed.temperature}°C
\n
Humidity: {parsed.humidity}%
\n
Wind Speed: {parsed.windSpeed ?? parsed.wind_speed} mph
\n
Conditions: {parsed.conditions}
\n
\n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Change background\",\n message: \"Change the background to something new.\",\n },\n {\n title: \"Generate sonnet\",\n message: \"Write a short sonnet about AI.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n \n
\n \n
\n \n );\n};\n\nexport default AgenticChat;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes two optional params — `start_custom_event`\nand `end_custom_event` — each accepting only an AG-UI `CustomEvent` instance\n(anything else raises `TypeError`), default `None` (off). When set, one\n`CUSTOM` event is emitted right after `RUN_STARTED`, another right before\n`RUN_FINISHED`. This demo's `OpenAIAgentsAgent` wrapper forwards them to\nreport fake usage split the way a real bill would be: `input_usage`\n(prompt tokens/cost — known before the model even runs) at the start,\n`output_usage` (completion tokens/cost — known only once the run is done)\nat the end — plain conversation otherwise, same agent as `agentic_chat`.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about AI.\"\n\nEach assistant reply has a compact usage label immediately above it. The label\nshows the input usage reported at the beginning of the run and the output usage\nreported at the end.\n\n## Technical Details\n\n- `build_input_usage_event()`/`build_output_usage_event()`\n (`agents_examples/custom_lifecycle_events.py`) build the two `CustomEvent`s\n for each run; `OpenAIAgentsAgent` calls the builders and passes the results\n to `to_agui(start_custom_event=..., end_custom_event=...)`\n- Both events are opaque to the translator — it only checks `isinstance(...,\n CustomEvent)`, the `name`/`value` shape is entirely up to the caller\n- The page captures the current `runId` from `RUN_STARTED`, associates the\n custom usage events with that run in a small external store, then uses\n `CopilotKitProvider`'s stable `renderCustomMessages` configuration to render\n the matching label above the assistant message\n- The token/cost numbers are fake — the point is the event bracketing, not\n real usage accounting\n", + "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", "language": "markdown", "type": "file" }, { - "name": "custom_lifecycle_events.py", - "content": "\"\"\"Custom lifecycle events — CUSTOM events bracketing the run.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's\nthe two builder functions below. The wrapper receives them as\n``start_custom_event``/``end_custom_event`` factories and forwards their\nresults to the same-named ``to_agui()`` parameters\nparams, so one\nCUSTOM event goes out right after RUN_STARTED and another right before\nRUN_FINISHED. Only CustomEvent instances are accepted there; anything else\nraises TypeError.\n\ninput_usage fires at the start because prompt tokens/cost are known before\nthe model even runs; output_usage fires at the end because completion\ntokens/cost are only known once the run is done. Numbers here are fake —\nthe point is the event bracketing, not real usage accounting.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui.core import CustomEvent, EventType\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_custom_lifecycle_events_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\ndef build_input_usage_value() -> dict[str, float]:\n tokens = random.randint(20, 120)\n return {\"tokens\": tokens, \"cost_usd\": round(tokens * 0.00000015, 6)}\n\n\ndef build_output_usage_value() -> dict[str, float]:\n tokens = random.randint(120, 480)\n return {\"tokens\": tokens, \"cost_usd\": round(tokens * 0.0000006, 6)}\n\n\nagent = OpenAIAgentsAgent(\n create_custom_lifecycle_events_agent(),\n name=\"custom_lifecycle_events\",\n start_custom_event=CustomEvent(\n type=EventType.CUSTOM, name=\"input_usage\", value=build_input_usage_value\n ),\n end_custom_event=CustomEvent(\n type=EventType.CUSTOM, name=\"output_usage\", value=build_output_usage_value\n ),\n)\napp = FastAPI(title=\"Custom lifecycle events AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "name": "agentic_chat.py", + "content": "\"\"\"Agentic chat — plain conversation, no tools.\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n\ndef create_agentic_chat_agent() -> Agent:\n return Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n )\n\n\nagent = OpenAIAgentsAgent(create_agentic_chat_agent(), name=\"agentic_chat\")\napp = FastAPI(title=\"Agentic chat AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } ], - "openai-agents-python::dynamic_system_prompt": [ + "openai-agents-python::backend_tool_rendering": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useAgentContext } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface DynamicSystemPromptProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ntype Language = \"English\" | \"Arabic\" | \"German\";\n\nconst LANGUAGES: { value: Language; label: string; flag: string }[] = [\n { value: \"English\", label: \"English\", flag: \"🇬🇧\" },\n { value: \"Arabic\", label: \"Arabic\", flag: \"🇸🇦\" },\n { value: \"German\", label: \"German\", flag: \"🇩🇪\" },\n];\n\nconst DynamicSystemPrompt: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DynamicSystemPromptView = () => {\n const [language, setLanguage] = useState(\"English\");\n\n // The only thing this demo sends: one context item the agent's\n // instructions callable reads on every turn to pick the reply language.\n // No tool, no state — just the AG-UI context channel.\n useAgentContext({\n description: \"Reply language\",\n value: language,\n });\n\n return (\n
\n
\n

Reply language

\n
\n {LANGUAGES.map(({ value, label, flag }) => (\n setLanguage(value)}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-sm text-left transition-colors ${\n language === value\n ? \"border-blue-400 bg-blue-50 dark:bg-blue-950/40\"\n : \"border-black/10 dark:border-white/10\"\n }`}\n >\n {flag}\n {label}\n \n ))}\n
\n

\n Switching language updates the AG-UI context sent with the next\n message — the agent's system prompt is rebuilt every turn to\n match.\n

\n
\n\n
\n
\n \n
\n
\n
\n );\n};\n\nexport default DynamicSystemPrompt;\n", + "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { \n useRenderTool,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n useRenderTool({\n \n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return (\n
\n ⚙️ Retrieving weather...\n
\n );\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field falls\n // through to its `|| 0` default and the card shows 0° C.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n const weatherResult: WeatherToolResult = {\n temperature: parsed.temperature ?? 0,\n conditions: parsed.conditions ?? \"clear\",\n humidity: parsed.humidity ?? 0,\n windSpeed: parsed.wind_speed ?? parsed.windSpeed ?? 0,\n feelsLike:\n parsed.feels_like ?? parsed.feelsLike ?? parsed.temperature ?? 0,\n };\n\n const themeColor = getThemeColor(weatherResult.conditions);\n\n return (\n \n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Weather in San Francisco\",\n message: \"What's the weather like in San Francisco?\",\n },\n {\n title: \"Weather in New York\",\n message: \"Tell me about the weather in New York.\",\n },\n {\n title: \"Weather in Tokyo\",\n message: \"How's the weather in Tokyo today?\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\ninterface WeatherToolResult {\n temperature: number;\n conditions: string;\n humidity: number;\n windSpeed: number;\n feelsLike: number;\n}\n\nfunction getThemeColor(conditions: string): string {\n const conditionLower = conditions.toLowerCase();\n if (conditionLower.includes(\"clear\") || conditionLower.includes(\"sunny\")) {\n return \"#667eea\";\n }\n if (conditionLower.includes(\"rain\") || conditionLower.includes(\"storm\")) {\n return \"#4A5568\";\n }\n if (conditionLower.includes(\"cloud\")) {\n return \"#718096\";\n }\n if (conditionLower.includes(\"snow\")) {\n return \"#63B3ED\";\n }\n return \"#764ba2\";\n}\n\nfunction WeatherCard({\n location,\n themeColor,\n result,\n status,\n}: {\n location?: string;\n themeColor: string;\n result: WeatherToolResult;\n status: \"inProgress\" | \"executing\" | \"complete\";\n}) {\n return (\n \n
\n
\n
\n

\n {location}\n

\n

Current Weather

\n
\n \n
\n\n
\n
\n {result.temperature}° C\n \n {\" / \"}\n {((result.temperature * 9) / 5 + 32).toFixed(1)}° F\n \n
\n
{result.conditions}
\n
\n\n
\n
\n
\n

Humidity

\n

{result.humidity}%

\n
\n
\n

Wind

\n

{result.windSpeed} mph

\n
\n
\n

Feels Like

\n

{result.feelsLike}°

\n
\n
\n
\n
\n \n );\n}\n\nfunction WeatherIcon({ conditions }: { conditions: string }) {\n if (!conditions) return null;\n\n if (conditions.toLowerCase().includes(\"clear\") || conditions.toLowerCase().includes(\"sunny\")) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"rain\") ||\n conditions.toLowerCase().includes(\"drizzle\") ||\n conditions.toLowerCase().includes(\"snow\") ||\n conditions.toLowerCase().includes(\"thunderstorm\")\n ) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"fog\") ||\n conditions.toLowerCase().includes(\"cloud\") ||\n conditions.toLowerCase().includes(\"overcast\")\n ) {\n return ;\n }\n\n return ;\n}\n\n// Simple sun icon for the weather card\nfunction SunIcon() {\n return (\n \n \n \n \n );\n}\n\nfunction RainIcon() {\n return (\n \n {/* Cloud */}\n \n {/* Rain drops */}\n \n \n );\n}\n\nfunction CloudIcon() {\n return (\n \n \n \n );\n}\n\nexport default AgenticChat;\n", "language": "typescript", "type": "file" }, + { + "name": "style.css", + "content": ".copilotKitInput {\n border-bottom-left-radius: 0.75rem;\n border-bottom-right-radius: 0.75rem;\n border-top-left-radius: 0.75rem;\n border-top-right-radius: 0.75rem;\n border: 1px solid var(--copilot-kit-separator-color) !important;\n}\n\n.copilotKitChat {\n background-color: #fff !important;\n}\n", + "language": "css", + "type": "file" + }, { "name": "README.mdx", - "content": "# 🌐 Dynamic System Prompt\n\n## What This Demo Shows\n\nThe frontend sends a language choice over the AG-UI **context** channel — a\nplain `{description, value}` pair, no special class involved. The agent's\n`instructions` is a callable (`Agent.instructions`, the OpenAI Agents SDK's\nown dynamic-instructions feature) that reads the context list every turn and\nbakes \"always reply in X\" into the system prompt fresh, before each model\ncall.\n\nContext needs no `AGUIContext`/state machinery — it's read-only and\nregenerated every run. The shared `OpenAIAgentsAgent` wrapper passes the\ntranslated context into `Runner.run_streamed(..., context=...)`, so this demo\nuses the same mounted FastAPI-app pattern as the other examples.\n\n## How to Interact\n\n- Pick a language in the sidebar, then ask anything — the reply comes back\n entirely in that language, regardless of what language you type in.\n- Switch languages mid-conversation and ask again — the next reply follows\n the new choice immediately, no restart needed.\n\n## Technical Details\n\n- `useAgentContext({ description: \"Reply language\", value: language })` —\n pushes the current pick onto `RunAgentInput.context` with every message\n- `dynamic_instructions(ctx, agent)` in\n `examples/agents_examples/dynamic_system_prompt.py` reads\n `ctx.context` (the raw `list[Context]` sent by the client) and returns a\n fresh instructions string\n- The demo app is mounted by `examples/server.py` at\n `/dynamic_system_prompt`; the aggregate OpenAI Agents server runs on port\n 8024\n", + "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", "language": "markdown", "type": "file" }, { - "name": "dynamic_system_prompt.py", - "content": "\"\"\"Dynamic system prompt — reply language driven by the AG-UI ``context`` channel.\n\nThe frontend sends a language choice in ``RunAgentInput.context`` and the SDK\nrebuilds its instructions for that request.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, RunContextWrapper\nfrom fastapi import FastAPI\n\nfrom ag_ui.core import Context\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\nBASE_INSTRUCTIONS = (\n \"You are a helpful, concise assistant. Answer the user's questions directly.\"\n)\n\n# Fallback when the frontend hasn't picked a language yet.\nDEFAULT_LANGUAGE = \"English\"\n\n\ndef _read_language(ctx: RunContextWrapper[list[Context]]) -> str:\n \"\"\"Pull the reply language out of the AG-UI context list.\n\n ``ctx.context`` here IS the raw ``list[Context]`` the client sent —\n each item a ``{description, value}`` pair, nothing wrapping it. We match\n the item whose description mentions \"language\" and use its value.\n \"\"\"\n items = ctx.context or []\n for item in items:\n if \"language\" in (item.description or \"\").lower():\n return item.value or DEFAULT_LANGUAGE\n return DEFAULT_LANGUAGE\n\n\ndef dynamic_instructions(ctx: RunContextWrapper[list[Context]], agent: Agent) -> str:\n \"\"\"Native SDK dynamic-instructions hook: build the prompt fresh each turn,\n baking in whatever language the frontend currently has selected.\"\"\"\n language = _read_language(ctx)\n return (\n f\"{BASE_INSTRUCTIONS}\\n\"\n f\"Always reply in {language}, no matter what language the user writes in. \"\n f\"Every word of your response must be in {language}.\"\n )\n\n\ndef create_dynamic_system_prompt_agent() -> Agent:\n return Agent(\n name=\"multilingual_assistant\",\n model=DEFAULT_MODEL,\n instructions=dynamic_instructions,\n )\n\n\nagent = OpenAIAgentsAgent(create_dynamic_system_prompt_agent(), name=\"dynamic_system_prompt\")\napp = FastAPI(title=\"Dynamic system prompt AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "name": "backend_tool_rendering.py", + "content": "\"\"\"Backend tool rendering — a server-side ``@function_tool``.\n\nExercises ``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` for a tool the\n*backend* owns and executes (as opposed to :mod:`human_in_the_loop`, where the\nfrontend owns execution). The SDK runs the tool body itself; the translator\njust reports the call and its result as they stream past.\n\nThe dojo's weather card (apps/dojo .../backend_tool_rendering/page.tsx) reads\nthe tool result as a JSON object — temperature/conditions/humidity/\nwind_speed/feels_like — and the argument as `location`, matching every other\nintegration's version of this demo. A plain sentence string or a `city`\nparam renders as a blank/all-zero card, since the frontend has nothing to\nparse. The fixed return value (same for every location) matches how most\nother integrations' versions of this demo work too — the point of the demo\nis the tool-call plumbing, not real weather data.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, function_tool\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n_WEATHER = {\"temperature\": 20, \"conditions\": \"sunny\", \"humidity\": 50, \"wind_speed\": 10, \"feels_like\": 20}\n\n\n@function_tool\ndef get_weather(location: str) -> dict:\n \"\"\"Get the current weather for a location.\"\"\"\n return _WEATHER\n\n\ndef create_backend_tool_agent() -> Agent:\n return Agent(\n name=\"weather_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a helpful weather assistant. Use the get_weather tool \"\n \"whenever the user asks about weather in a specific location.\"\n ),\n tools=[get_weather],\n )\n\n\nagent = OpenAIAgentsAgent(create_backend_tool_agent(), name=\"backend_tool_rendering\")\napp = FastAPI(title=\"Backend tool rendering AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } ], - "langroid::agentic_chat": [ + "openai-agents-python::human_in_the_loop": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useRenderTool,\n useAgentContext,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n\n useAgentContext({\n description: 'Name of the user',\n value: 'Bob'\n });\n\n useFrontendTool({\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients. Only use when asked.\"),\n }) ,\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n return {\n status: \"success\",\n message: `Background changed to ${background}`,\n };\n },\n });\n\n useRenderTool({\n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return
Loading weather...
;\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field reads as\n // undefined and the card renders empty values.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n return (\n
\n Weather in {parsed.city ?? args.location}\n
Temperature: {parsed.temperature}°C
\n
Humidity: {parsed.humidity}%
\n
Wind Speed: {parsed.windSpeed ?? parsed.wind_speed} mph
\n
Conditions: {parsed.conditions}
\n
\n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Change background\",\n message: \"Change the background to something new.\",\n },\n {\n title: \"Generate sonnet\",\n message: \"Write a short sonnet about AI.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n \n
\n \n
\n \n );\n};\n\nexport default AgenticChat;\n", + "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useHumanInTheLoop,\n useConfigureSuggestions,\n CopilotChat,\n CopilotChatConfigurationProvider,\n} from \"@copilotkit/react-core/v2\";\nimport { CopilotKit,\nuseLangGraphInterrupt } from \"@copilotkit/react-core\";\nimport { z } from \"zod\";\nimport { useTheme } from \"next-themes\";\n\ninterface HumanInTheLoopProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst HumanInTheLoop: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\ninterface Step {\n description: string;\n status: \"disabled\" | \"enabled\" | \"executing\";\n}\n\n// Shared UI Components\nconst StepContainer = ({ theme, children }: { theme?: string; children: React.ReactNode }) => (\n
\n \n {children}\n
\n \n);\n\nconst StepHeader = ({\n theme,\n enabledCount,\n totalCount,\n status,\n showStatus = false,\n}: {\n theme?: string;\n enabledCount: number;\n totalCount: number;\n status?: string;\n showStatus?: boolean;\n}) => (\n
\n
\n

\n Select Steps\n

\n
\n
\n {enabledCount}/{totalCount} Selected\n
\n {showStatus && (\n \n {status === \"executing\" ? \"Ready\" : \"Waiting\"}\n
\n )}\n
\n
\n\n \n 0 ? (enabledCount / totalCount) * 100 : 0}%` }}\n />\n \n \n);\n\nconst StepItem = ({\n step,\n theme,\n status,\n onToggle,\n disabled = false,\n}: {\n step: { description: string; status: string };\n theme?: string;\n status?: string;\n onToggle: () => void;\n disabled?: boolean;\n}) => (\n \n \n \n);\n\nconst ActionButton = ({\n variant,\n theme,\n disabled,\n onClick,\n children,\n}: {\n variant: \"primary\" | \"secondary\" | \"success\" | \"danger\";\n theme?: string;\n disabled?: boolean;\n onClick: () => void;\n children: React.ReactNode;\n}) => {\n const baseClasses = \"px-6 py-3 rounded-lg font-semibold transition-all duration-200\";\n const enabledClasses = \"hover:scale-105 shadow-md hover:shadow-lg\";\n const disabledClasses = \"opacity-50 cursor-not-allowed\";\n\n const variantClasses = {\n primary:\n \"bg-gradient-to-r from-purple-500 to-purple-700 hover:from-purple-600 hover:to-purple-800 text-white shadow-lg hover:shadow-xl\",\n secondary:\n theme === \"dark\"\n ? \"bg-slate-700 hover:bg-slate-600 text-white border border-slate-600 hover:border-slate-500\"\n : \"bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-300 hover:border-gray-400\",\n success:\n \"bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white shadow-lg hover:shadow-xl\",\n danger:\n \"bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white shadow-lg hover:shadow-xl\",\n };\n\n return (\n \n {children}\n \n );\n};\n\nconst DecorativeElements = ({\n theme,\n variant = \"default\",\n}: {\n theme?: string;\n variant?: \"default\" | \"success\" | \"danger\";\n}) => (\n <>\n \n \n \n);\nconst InterruptHumanInTheLoop: React.FC<{\n event: { value: { steps: Step[] } };\n resolve: (value: string) => void;\n}> = ({ event, resolve }) => {\n const { theme } = useTheme();\n\n // Parse and initialize steps data\n let initialSteps: Step[] = [];\n if (event.value && event.value.steps && Array.isArray(event.value.steps)) {\n initialSteps = event.value.steps.map((step: any) => ({\n description: typeof step === \"string\" ? step : step.description || \"\",\n status: typeof step === \"object\" && step.status ? step.status : \"enabled\",\n }));\n }\n\n const [localSteps, setLocalSteps] = useState(initialSteps);\n const enabledCount = localSteps.filter((step) => step.status === \"enabled\").length;\n\n const handleStepToggle = (index: number) => {\n setLocalSteps((prevSteps) =>\n prevSteps.map((step, i) =>\n i === index\n ? { ...step, status: step.status === \"enabled\" ? \"disabled\" : \"enabled\" }\n : step,\n ),\n );\n };\n\n const handlePerformSteps = () => {\n const selectedSteps = localSteps\n .filter((step) => step.status === \"enabled\")\n .map((step) => step.description);\n resolve(\"The user selected the following steps: \" + selectedSteps.join(\", \"));\n };\n\n return (\n \n \n\n
\n {localSteps.map((step, index) => (\n handleStepToggle(index)}\n />\n ))}\n
\n\n
\n \n \n Perform Steps\n \n {enabledCount}\n \n \n
\n\n \n
\n );\n};\n\nconst Chat = ({ integrationId }: { integrationId: string }) => {\n return (\n \n \n \n );\n};\n\nconst ChatContent = () => {\n useConfigureSuggestions({\n suggestions: [\n { title: \"Simple plan\", message: \"Please plan a trip to mars in 5 steps.\" },\n { title: \"Complex plan\", message: \"Please plan a pasta dish in 10 steps.\" },\n ],\n available: \"always\",\n });\n\n // Langgraph uses it's own hook to handle human-in-the-loop interactions via langgraph interrupts,\n // This hook won't do anything for other integrations.\n useLangGraphInterrupt({\n \n render: ({ event, resolve }) => ,\n });\n useHumanInTheLoop({\n agentId: \"human_in_the_loop\",\n name: \"generate_task_steps\",\n description: \"Generates a list of steps for the user to perform\",\n parameters: z.object({\n steps: z.array(\n z.object({\n description: z.string(),\n status: z.enum([\"enabled\", \"disabled\", \"executing\"]),\n }),\n ),\n }) ,\n // Note: In v1, `available` was used to disable this for langgraph integrations.\n // In v2, availability is handled at the agent/backend level.\n render: ({ args, respond, status }: any) => {\n return ;\n },\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nconst StepsFeedback = ({ args, respond, status }: { args: any; respond: any; status: any }) => {\n const { theme } = useTheme();\n const [localSteps, setLocalSteps] = useState([]);\n const [accepted, setAccepted] = useState(null);\n\n useEffect(() => {\n if (status === \"executing\" && localSteps.length === 0 && Array.isArray(args?.steps) && args.steps.length > 0) {\n setLocalSteps(args.steps);\n }\n }, [status, args?.steps, localSteps]);\n\n if (!Array.isArray(args?.steps) || args.steps.length === 0) {\n return <>;\n }\n\n const steps = Array.isArray(localSteps) && localSteps.length > 0 ? localSteps : args.steps;\n const enabledCount = steps.filter((step: any) => step.status === \"enabled\").length;\n\n const handleStepToggle = (index: number) => {\n setLocalSteps((prevSteps) =>\n prevSteps.map((step, i) =>\n i === index\n ? { ...step, status: step.status === \"enabled\" ? \"disabled\" : \"enabled\" }\n : step,\n ),\n );\n };\n\n const handleReject = () => {\n if (respond) {\n setAccepted(false);\n respond({ accepted: false });\n }\n };\n\n const handleConfirm = () => {\n if (respond) {\n const confirmedSteps = localSteps.filter((step) => step.status === \"enabled\");\n setAccepted(true);\n respond({ accepted: true, steps: confirmedSteps });\n }\n };\n\n return (\n \n \n\n
\n {steps.map((step: any, index: any) => (\n handleStepToggle(index)}\n disabled={status !== \"executing\"}\n />\n ))}\n
\n\n {/* Action Buttons - Different logic from InterruptHumanInTheLoop */}\n {accepted === null && (\n
\n \n \n Reject\n \n \n \n Confirm\n \n {enabledCount}\n \n \n
\n )}\n\n {/* Result State - Unique to StepsFeedback */}\n {accepted !== null && (\n
\n \n {accepted ? \"✓\" : \"✗\"}\n {accepted ? \"Accepted\" : \"Rejected\"}\n
\n \n )}\n\n \n
\n );\n};\n\nexport default HumanInTheLoop;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", + "content": "# 🤝 Human-in-the-Loop Task Planner\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **human-in-the-loop** capabilities:\n\n1. **Collaborative Planning**: The Copilot generates task steps and lets you\n decide which ones to perform\n2. **Interactive Decision Making**: Select or deselect steps to customize the\n execution plan\n3. **Adaptive Responses**: The Copilot adapts its execution based on your\n choices, even handling missing steps\n\n## How to Interact\n\nTry these steps to experience the demo:\n\n1. Ask your Copilot to help with a task, such as:\n\n - \"Make me a sandwich\"\n - \"Plan a weekend trip\"\n - \"Organize a birthday party\"\n - \"Start a garden\"\n\n2. Review the suggested steps provided by your Copilot\n\n3. Select or deselect steps using the checkboxes to customize the plan\n\n - Try removing essential steps to see how the Copilot adapts!\n\n4. Click \"Execute Plan\" to see the outcome based on your selections\n\n## ✨ Human-in-the-Loop Magic in Action\n\n**What's happening technically:**\n\n- The agent analyzes your request and breaks it down into logical steps\n- These steps are presented to you through a dynamic UI component\n- Your selections are captured as user input\n- The agent considers your choices when executing the plan\n- The agent adapts to missing steps with creative problem-solving\n\n**What you'll see in this demo:**\n\n- The Copilot provides a detailed, step-by-step plan for your task\n- You have complete control over which steps to include\n- If you remove essential steps, the Copilot provides entertaining and creative\n workarounds\n- The final execution reflects your choices, showing how human input shapes the\n outcome\n- Each response is tailored to your specific selections\n\nThis human-in-the-loop pattern creates a powerful collaborative experience where\nboth human judgment and AI capabilities work together to achieve better results\nthan either could alone!\n", "language": "markdown", "type": "file" }, { - "name": "agentic_chat.py", - "content": "\"\"\"Agentic Chat example for Langroid.\n\nSimple conversational agent with change_background frontend tool.\n\"\"\"\nimport os\nfrom pathlib import Path\nfrom dotenv import load_dotenv\n\nenv_path = Path(__file__).parent.parent.parent / '.env'\nload_dotenv(dotenv_path=env_path)\n\nimport langroid as lr\nfrom langroid.agent import ToolMessage\nfrom langroid.language_models import OpenAIChatModel\nfrom ag_ui_langroid import LangroidAgent, create_langroid_app\n\n\nclass ChangeBackgroundTool(ToolMessage):\n request: str = \"change_background\"\n purpose: str = \"\"\"\n Change the background color of the chat. Can be anything that the CSS background\n attribute accepts. Regular colors, linear or radial gradients etc.\n Only use when the user explicitly asks to change the background.\n \"\"\"\n background: str\n\nllm_config = lr.language_models.OpenAIGPTConfig(\n chat_model=OpenAIChatModel.GPT4_1_MINI,\n api_key=os.getenv(\"OPENAI_API_KEY\"),\n temperature=0.0,\n)\n\nagent_config = lr.ChatAgentConfig(\n name=\"Assistant\",\n llm=llm_config,\n system_message=\"\"\"You are a helpful assistant. \nWhen you change the background, always confirm the action to the user with a friendly message like 'I've changed the background to [color/gradient] for you!' or similar.\"\"\",\n use_tools=True,\n use_functions_api=True,\n)\n\nchat_agent = lr.ChatAgent(agent_config)\nchat_agent.enable_message(ChangeBackgroundTool)\n\ntask = lr.Task(\n chat_agent,\n name=\"Assistant\",\n interactive=False,\n single_round=False,\n)\n\nagui_agent = LangroidAgent(\n agent=task,\n name=\"agentic_chat\",\n description=\"Simple conversational Langroid agent with frontend tools\",\n)\n\napp = create_langroid_app(agui_agent, \"/\")\n\n", + "name": "human_in_the_loop.py", + "content": "\"\"\"Human-in-the-loop — a *frontend*-owned tool, approved before execution.\n\nUnlike :mod:`backend_tool_rendering`, ``generate_task_steps`` has no server\nimplementation — it arrives per-request as an AG-UI client tool\n(``RunAgentInput.tools``), gets wrapped into an SDK ``FunctionTool`` proxy by\n``AGUIToOpenAITranslator.translate_tools()``, and is merged onto this agent by\nthe run loop (see ``server.py``).\n\n``tool_use_behavior=StopAtTools(...)`` is the SDK's built-in for \"the model\nmay only *call* this tool, never execute it\": the run ends the moment the\nmodel emits the call, before the (dead-code) proxy body would ever run. The\nfrontend renders the steps for user approval, then sends the result back as\nan AG-UI ``ToolMessage`` in the *next* request — ordinary multi-turn history,\nno custom pause/resume machinery needed.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_task_steps\"\n\nINSTRUCTIONS = \"\"\"You are a task planning assistant that breaks work into clear, actionable steps.\n\nWhen the user asks for help with a task:\n1. Immediately call the `generate_task_steps` tool with an array of steps.\n Each step is an object: {\"description\": \"...\", \"status\": \"enabled\"}.\n2. Do not restate the steps as plain text — the frontend renders them.\n3. After the call, wait for the user to approve/select steps; do not call\n the tool again until they respond.\n\"\"\"\n\n\ndef create_human_in_the_loop_agent() -> Agent:\n return Agent(\n name=\"task_planner\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n\n\nagent = OpenAIAgentsAgent(create_human_in_the_loop_agent(), name=\"human_in_the_loop\")\napp = FastAPI(title=\"Human in the loop AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } ], - "langroid::backend_tool_rendering": [ + "openai-agents-python::human_in_the_loop_approval": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { \n useRenderTool,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n useRenderTool({\n \n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return (\n
\n ⚙️ Retrieving weather...\n
\n );\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field falls\n // through to its `|| 0` default and the card shows 0° C.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n const weatherResult: WeatherToolResult = {\n temperature: parsed.temperature ?? 0,\n conditions: parsed.conditions ?? \"clear\",\n humidity: parsed.humidity ?? 0,\n windSpeed: parsed.wind_speed ?? parsed.windSpeed ?? 0,\n feelsLike:\n parsed.feels_like ?? parsed.feelsLike ?? parsed.temperature ?? 0,\n };\n\n const themeColor = getThemeColor(weatherResult.conditions);\n\n return (\n \n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Weather in San Francisco\",\n message: \"What's the weather like in San Francisco?\",\n },\n {\n title: \"Weather in New York\",\n message: \"Tell me about the weather in New York.\",\n },\n {\n title: \"Weather in Tokyo\",\n message: \"How's the weather in Tokyo today?\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\ninterface WeatherToolResult {\n temperature: number;\n conditions: string;\n humidity: number;\n windSpeed: number;\n feelsLike: number;\n}\n\nfunction getThemeColor(conditions: string): string {\n const conditionLower = conditions.toLowerCase();\n if (conditionLower.includes(\"clear\") || conditionLower.includes(\"sunny\")) {\n return \"#667eea\";\n }\n if (conditionLower.includes(\"rain\") || conditionLower.includes(\"storm\")) {\n return \"#4A5568\";\n }\n if (conditionLower.includes(\"cloud\")) {\n return \"#718096\";\n }\n if (conditionLower.includes(\"snow\")) {\n return \"#63B3ED\";\n }\n return \"#764ba2\";\n}\n\nfunction WeatherCard({\n location,\n themeColor,\n result,\n status,\n}: {\n location?: string;\n themeColor: string;\n result: WeatherToolResult;\n status: \"inProgress\" | \"executing\" | \"complete\";\n}) {\n return (\n \n
\n
\n
\n

\n {location}\n

\n

Current Weather

\n
\n \n
\n\n
\n
\n {result.temperature}° C\n \n {\" / \"}\n {((result.temperature * 9) / 5 + 32).toFixed(1)}° F\n \n
\n
{result.conditions}
\n
\n\n
\n
\n
\n

Humidity

\n

{result.humidity}%

\n
\n
\n

Wind

\n

{result.windSpeed} mph

\n
\n
\n

Feels Like

\n

{result.feelsLike}°

\n
\n
\n
\n
\n \n );\n}\n\nfunction WeatherIcon({ conditions }: { conditions: string }) {\n if (!conditions) return null;\n\n if (conditions.toLowerCase().includes(\"clear\") || conditions.toLowerCase().includes(\"sunny\")) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"rain\") ||\n conditions.toLowerCase().includes(\"drizzle\") ||\n conditions.toLowerCase().includes(\"snow\") ||\n conditions.toLowerCase().includes(\"thunderstorm\")\n ) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"fog\") ||\n conditions.toLowerCase().includes(\"cloud\") ||\n conditions.toLowerCase().includes(\"overcast\")\n ) {\n return ;\n }\n\n return ;\n}\n\n// Simple sun icon for the weather card\nfunction SunIcon() {\n return (\n \n \n \n \n );\n}\n\nfunction RainIcon() {\n return (\n \n {/* Cloud */}\n \n {/* Rain drops */}\n \n \n );\n}\n\nfunction CloudIcon() {\n return (\n \n \n \n );\n}\n\nexport default AgenticChat;\n", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n useAgent,\n useCopilotKit,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface ApprovalProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface PendingApproval {\n callId: string;\n toolName: string;\n arguments: string;\n}\n\nconst Approval: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"human_in_the_loop_approval\" });\n const { copilotkit } = useCopilotKit();\n const [pending, setPending] = useState(null);\n const [resolvedCallId, setResolvedCallId] = useState(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Refund an order\", message: \"I'd like a refund for ORD-1001.\" },\n { title: \"Refund another order\", message: \"Please refund ORD-1002.\" },\n { title: \"Unknown order\", message: \"Can you refund ORD-9999?\" },\n ],\n available: \"always\",\n consumerAgentId: \"human_in_the_loop_approval\",\n });\n\n React.useEffect(() => {\n const subscription = agent.subscribe({\n onCustomEvent: ({ event }) => {\n if (event.name !== \"approval_request\") return;\n // One CustomEvent can carry several pending calls if the model made\n // more than one needs_approval call in a turn; this demo only ever\n // triggers one, so showing the first is enough here.\n const pendingList = event.value as Array<{\n call_id: string;\n tool_name: string;\n arguments: string;\n }>;\n const first = pendingList[0];\n if (!first) return;\n setResolvedCallId(null);\n setPending({\n callId: first.call_id,\n toolName: first.tool_name,\n arguments: first.arguments,\n });\n },\n onRunFinishedEvent: () => setResolvedCallId(null),\n onRunErrorEvent: () => setResolvedCallId(null),\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n const respond = (approve: boolean) => {\n if (!pending) return;\n setResolvedCallId(pending.callId);\n copilotkit.runAgent({\n agent,\n forwardedProps: {\n approval: { call_id: pending.callId, approve },\n },\n });\n setPending(null);\n };\n\n return (\n
\n {pending && (\n respond(true)}\n onReject={() => respond(false)}\n />\n )}\n {!pending && resolvedCallId && (\n
\n Decision sent — waiting for the agent...\n
\n )}\n
\n \n
\n
\n );\n};\n\nfunction ApprovalCard({\n pending,\n onApprove,\n onReject,\n}: {\n pending: PendingApproval;\n onApprove: () => void;\n onReject: () => void;\n}) {\n let args: Record = {};\n try {\n args = JSON.parse(pending.arguments);\n } catch {\n // leave args empty — raw string shown below is enough context either way\n }\n\n return (\n \n

Approval needed

\n

\n The agent wants to call{\" \"}\n {pending.toolName} with:\n

\n
\n        {JSON.stringify(args, null, 2)}\n      
\n
\n \n Reject\n \n \n Approve\n \n
\n \n );\n}\n\nexport default Approval;\n", "language": "typescript", "type": "file" }, - { - "name": "style.css", - "content": ".copilotKitInput {\n border-bottom-left-radius: 0.75rem;\n border-bottom-right-radius: 0.75rem;\n border-top-left-radius: 0.75rem;\n border-top-right-radius: 0.75rem;\n border: 1px solid var(--copilot-kit-separator-color) !important;\n}\n\n.copilotKitChat {\n background-color: #fff !important;\n}\n", - "language": "css", - "type": "file" - }, { "name": "README.mdx", - "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", + "content": "---\ntitle: Human in the Loop Approval\ndescription: Pause a backend tool call until a person approves or rejects it.\n---\n\n## Human in the Loop Approval\n\nThis example uses a backend-owned `issue_refund` tool with the OpenAI Agents\nSDK's `needs_approval` option. When the agent asks to issue a refund, the run\npauses and the page shows an approval card.\n\nChoose **Approve** to execute the refund or **Reject** to resume the agent\nwithout executing it. The server keeps the paused SDK state for the thread and\nuses the decision from the next request to continue that same run.\n", "language": "markdown", "type": "file" }, { - "name": "backend_tool_rendering.py", - "content": "\"\"\"Backend Tool Rendering example for Langroid.\n\nThis example shows an agent with backend tool rendering capabilities.\nBackend tools are executed on the server side, and the results are returned to the agent.\n\"\"\"\nimport json\nimport os\nimport random\nfrom pathlib import Path\nfrom dotenv import load_dotenv\n\nenv_path = Path(__file__).parent.parent.parent / '.env'\nload_dotenv(dotenv_path=env_path)\n\nimport langroid as lr\nfrom langroid.agent import ToolMessage, ChatAgent\nfrom langroid.language_models import OpenAIChatModel\nfrom ag_ui_langroid import LangroidAgent, create_langroid_app\n\n\nclass GetWeatherTool(ToolMessage):\n \"\"\"Get weather information for a location.\"\"\"\n request: str = \"get_weather\"\n purpose: str = \"\"\"\n Get current weather information for a specific location.\n Use this when the user asks about weather conditions.\n \"\"\"\n location: str\n\nclass RenderChartTool(ToolMessage):\n \"\"\"Render a chart with backend processing.\"\"\"\n request: str = \"render_chart\"\n purpose: str = \"\"\"\n Render a chart with backend processing capabilities.\n Use this when the user wants to visualize data in a chart format.\n \"\"\"\n chart_type: str\n data: str\n\n\nllm_config = lr.language_models.OpenAIGPTConfig(\n chat_model=OpenAIChatModel.GPT4_1_MINI,\n api_key=os.getenv(\"OPENAI_API_KEY\"),\n # Make behavior deterministic for demos and e2e tests\n temperature=0.0,\n)\n\n\n\nagent_config = lr.ChatAgentConfig(\n name=\"WeatherAssistant\",\n llm=llm_config,\n system_message=\"\"\"You are a helpful assistant with backend tool rendering capabilities.\n You can get weather information and render charts.\n\n CRITICAL RULES:\n - When the user asks about the weather for a specific location, you MUST call the `get_weather` tool EXACTLY ONCE.\n - Do NOT answer with current weather details unless you have first called `get_weather` and used the returned JSON.\n - When describing weather data, use the EXACT values from the tool result (temperature, conditions, humidity, wind speed, feels_like, location).\n - Never tell the user you are going to fetch or retrieve weather data without actually calling the `get_weather` tool.\n - When the user asks to visualize or chart data, you MUST call the `render_chart` tool to generate the chart metadata.\n - After calling a tool, provide a brief natural-language summary that is fully consistent with the tool result.\n \"\"\",\n use_tools=True,\n use_functions_api=True,\n)\n\n\nclass WeatherAssistantAgent(ChatAgent):\n \"\"\"ChatAgent with backend tool handlers.\"\"\"\n \n def get_weather(self, msg: GetWeatherTool) -> str:\n \"\"\"Handle get_weather tool execution. Returns JSON string with weather data.\"\"\"\n location = msg.location\n conditions_list = [\"sunny\", \"cloudy\", \"rainy\", \"clear\", \"partly cloudy\"]\n result = {\n \"temperature\": random.randint(60, 85),\n \"conditions\": random.choice(conditions_list),\n \"humidity\": random.randint(30, 80),\n \"wind_speed\": random.randint(5, 20),\n \"feels_like\": random.randint(58, 88),\n \"location\": location\n }\n return json.dumps(result)\n \n def render_chart(self, msg: RenderChartTool) -> str:\n \"\"\"Handle render_chart tool execution. Returns JSON string with chart data.\"\"\"\n chart_type = msg.chart_type\n data = msg.data\n result = {\n \"chart_type\": chart_type,\n \"data_preview\": data[:100] if len(data) > 100 else data,\n \"status\": \"rendered\",\n \"message\": f\"Successfully rendered {chart_type} chart\"\n }\n return json.dumps(result)\n\n\nchat_agent = WeatherAssistantAgent(agent_config)\nchat_agent.enable_message(GetWeatherTool)\nchat_agent.enable_message(RenderChartTool)\n\ntask = lr.Task(\n chat_agent,\n name=\"WeatherAssistant\",\n interactive=False,\n single_round=False,\n)\n\nagui_agent = LangroidAgent(\n agent=task,\n name=\"backend_tool_rendering\",\n description=\"Langroid agent with backend tool rendering support - weather and chart rendering\",\n)\n\napp = create_langroid_app(agui_agent, \"/\")\n\n", + "name": "human_in_the_loop_approval.py", + "content": "\"\"\"Tool approval — a *backend*-owned tool gated by the SDK's own approval API.\n\nUnlike :mod:`human_in_the_loop` (a frontend-only tool with no server\nimplementation) and :mod:`backend_tool_rendering` (a server tool that always\nruns), ``issue_refund`` here is real server-side logic that only runs after a\nhuman approves it — the SDK's ``needs_approval=True`` mechanism\n(``agents.tool.function_tool``), not an AG-UI concept.\n\nMechanically: when the model calls ``issue_refund``, the SDK stops the run\n*before* the tool body executes and surfaces a ``ToolApprovalItem`` on\n``result.interruptions``. That only becomes known once the stream is fully\ndrained — there is no mid-stream event for it — so it can't go through the\nnormal per-item translator dispatch the way ``MCPApprovalRequestItem`` does.\nThe example's run loop checks\n``result.interruptions`` right after ``to_agui()`` finishes, and if any are\npending:\n\n1. Serializes the paused run via ``result.to_state()`` and keeps it\n server-side, keyed by ``thread_id`` (an in-memory dict here — a real app\n would use a session store; this survives one process, not a restart).\n2. Emits one ``CustomEvent(name=\"approval_request\")`` carrying every\n interruption, as ``to_agui()``'s ``end_custom_event`` — right before\n ``RUN_FINISHED``, not after it. The client drops anything that arrives\n once a run is marked finished, so this has to land before that event,\n which means draining the raw SDK stream by hand first (interruptions\n aren't known until it's fully drained) instead of handing ``result``\n straight to ``to_agui()``.\n\nThe frontend renders Approve/Reject; either choice comes back as the next\n``RunAgentInput.forwarded_props[\"approval\"]`` (``{\"call_id\", \"approve\"}``).\nThe aggregate server looks up the stored state, calls ``state.approve()`` /\n``state.reject()``, and resumes with ``Runner.run_streamed(agent, state)``\ninstead of starting fresh from ``translated.messages``.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom ag_ui_openai_agents.engine import ClientToolPending\nfrom .constants import DEFAULT_MODEL\n\nlogger = logging.getLogger(__name__)\n\n# Fake order book — good enough to make \"approved\" visibly do something.\n_ORDERS: dict[str, dict] = {\n \"ORD-1001\": {\"amount\": 49.99, \"status\": \"paid\"},\n \"ORD-1002\": {\"amount\": 129.50, \"status\": \"paid\"},\n}\n\n\n@function_tool(needs_approval=True)\ndef issue_refund(order_id: str) -> str:\n \"\"\"Issue a full refund for an order. Requires human approval before running.\"\"\"\n order = _ORDERS.get(order_id)\n if order is None:\n return f\"No such order: {order_id}\"\n order[\"status\"] = \"refunded\"\n return f\"Refunded ${order['amount']:.2f} for {order_id}.\"\n\n\ndef create_human_in_the_loop_approval_agent() -> Agent:\n return Agent(\n name=\"refund_assistant\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You are a customer support assistant. When the user asks for a \"\n \"refund on an order, call issue_refund with that order id. \"\n \"Known orders: ORD-1001 ($49.99), ORD-1002 ($129.50). Don't ask \"\n \"for confirmation yourself — the approval happens outside the \"\n \"conversation before the tool runs.\"\n ),\n tools=[issue_refund],\n )\n\n\nagent = create_human_in_the_loop_approval_agent()\napp = FastAPI(title=\"Human in the loop approval AG-UI demo\")\n_translator = AGUITranslator()\n_encoder = EventEncoder()\n_pending_approvals: dict[str, object] = {}\n\n\ndef resolve_approval(\n store: dict[str, Any],\n thread_id: str,\n forwarded_props: Any,\n) -> tuple[Any, Any, bool]:\n \"\"\"Claim the paused run for this request when a matching decision arrived.\n\n Pops first: whoever gets here wins, so a double-clicked Approve can't\n resume the same run twice. Anything other than a decision that matches a\n pending interruption abandons the paused run and starts a fresh turn —\n the user moved on, and a thread should never be stuck waiting forever.\n\n ``approve`` has to be a real bool. Truthiness would read the string\n \"false\" as approval and run a refund the user declined, so a malformed\n decision is treated as no decision at all.\n\n Args:\n store: thread_id -> paused RunState.\n thread_id: The thread this request belongs to.\n forwarded_props: The request's forwarded_props.\n\n Returns:\n (pending_state, item, approve) to resume, or (None, None, False) to\n run the request fresh.\n \"\"\"\n pending_state = store.pop(thread_id, None)\n if pending_state is None:\n return None, None, False\n\n decision = None\n if isinstance(forwarded_props, dict):\n decision = forwarded_props.get(\"approval\")\n if not isinstance(decision, dict):\n return None, None, False\n\n approve = decision.get(\"approve\")\n if not isinstance(approve, bool):\n return None, None, False\n\n item = next(\n (\n item\n for item in pending_state.get_interruptions()\n if getattr(item.raw_item, \"call_id\", None) == decision.get(\"call_id\")\n ),\n None,\n )\n if item is None:\n return None, None, False\n return pending_state, item, approve\n\n\n@app.post(\"/\")\nasync def run(body: RunAgentInput) -> StreamingResponse:\n \"\"\"Run or resume the approval-gated agent.\"\"\"\n\n pending_state, item, approve = resolve_approval(\n _pending_approvals, body.thread_id, body.forwarded_props\n )\n\n async def stream():\n if item is not None:\n if approve:\n pending_state.approve(item)\n else:\n pending_state.reject(item)\n result = Runner.run_streamed(agent, pending_state)\n else:\n translated = _translator.to_openai(body)\n run_agent = agent\n if translated.tools:\n run_agent = run_agent.clone(tools=[*agent.tools, *translated.tools])\n result = Runner.run_streamed(\n run_agent, input=translated.messages, context=translated.context\n )\n\n # Collect as we go rather than in one comprehension: whatever streamed\n # before a mid-drain stop still has to reach the client. A client-owned\n # tool ends the run cleanly here; a real failure is kept and re-raised\n # from replay() below, so to_agui sees it and handles it the same way\n # it would on any other route.\n raw_events = []\n stream_error = None\n try:\n async for event in result.stream_events():\n raw_events.append(event)\n except ClientToolPending:\n pass\n except Exception as exc:\n stream_error = exc\n\n end_custom_event = None\n if stream_error is None and result.interruptions:\n _pending_approvals[body.thread_id] = result.to_state()\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM,\n name=\"approval_request\",\n value=[\n {\n \"call_id\": getattr(item.raw_item, \"call_id\", None),\n \"tool_name\": item.tool_name,\n \"arguments\": getattr(item.raw_item, \"arguments\", None),\n }\n for item in result.interruptions\n ],\n )\n\n async def replay():\n for event in raw_events:\n yield event\n if stream_error is not None:\n raise stream_error\n\n try:\n async for event in _translator.to_agui(\n replay(), body, end_custom_event=end_custom_event\n ):\n yield _encoder.encode(event)\n except Exception:\n # to_agui already sent RUN_ERROR before re-raising; log the real\n # traceback here rather than let it escape the response.\n logger.exception(\"Agent run failed\")\n\n return StreamingResponse(stream(), media_type=_encoder.get_content_type())\n", "language": "python", "type": "file" } ], - "langroid::agentic_generative_ui": [ + "openai-agents-python::tool_based_generative_ui": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { \n useAgent,\n UseAgentUpdate,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { useTheme } from \"next-themes\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticGenerativeUIProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticGenerativeUI: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n return (\n \n \n \n );\n};\n\ninterface AgentState {\n steps: {\n description: string;\n status: \"pending\" | \"completed\";\n }[];\n}\n\nconst Chat = () => {\n const { theme } = useTheme();\n const { agent } = useAgent({\n agentId: \"agentic_generative_ui\",\n updates: [UseAgentUpdate.OnStateChanged],\n });\n\n const agentState = agent.state as AgentState | undefined;\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Simple plan\",\n message: \"Please build a plan to go to mars in 5 steps.\",\n },\n {\n title: \"Complex plan\",\n message: \"Please build a plan to go to make pizza in 10 steps.\",\n },\n ],\n available: \"always\",\n });\n\n const steps = agentState?.steps;\n\n return (\n
\n
\n (\n
\n {messageElements}\n {steps && steps.length > 0 && (\n
\n \n
\n )}\n {interruptElement}\n
\n ),\n }}\n />\n
\n
\n );\n};\n\nfunction TaskProgress({ steps, theme }: { steps: AgentState[\"steps\"]; theme?: string }) {\n const completedCount = steps.filter((step) => step.status === \"completed\").length;\n const progressPercentage = (completedCount / steps.length) * 100;\n\n return (\n
\n \n {/* Header */}\n
\n
\n

\n Task Progress\n

\n
\n {completedCount}/{steps.length} Complete\n
\n
\n\n {/* Progress Bar */}\n \n \n \n
\n
\n\n {/* Steps */}\n
\n {steps.map((step, index) => {\n const isCompleted = step.status === \"completed\";\n const isCurrentPending =\n step.status === \"pending\" &&\n index === steps.findIndex((s) => s.status === \"pending\");\n const isFuturePending = step.status === \"pending\" && !isCurrentPending;\n\n return (\n \n {/* Connector Line */}\n {index < steps.length - 1 && (\n \n )}\n\n {/* Status Icon */}\n \n {isCompleted ? (\n \n ) : isCurrentPending ? (\n \n ) : (\n \n )}\n
\n\n {/* Step Content */}\n
\n \n {step.description}\n
\n {isCurrentPending && (\n \n Processing...\n \n )}\n \n\n {/* Animated Background for Current Step */}\n {isCurrentPending && (\n \n )}\n \n );\n })}\n \n\n {/* Decorative Elements */}\n \n \n \n \n );\n}\n\n// Enhanced Icons\nfunction CheckIcon() {\n return (\n \n \n \n );\n}\n\nfunction SpinnerIcon() {\n return (\n \n \n \n \n );\n}\n\nfunction ClockIcon({ theme }: { theme?: string }) {\n return (\n \n \n \n \n );\n}\n\nexport default AgenticGenerativeUI;\n", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useConfigureSuggestions,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport {\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselNext,\n CarouselPrevious,\n} from \"@/components/ui/carousel\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface ToolBasedGenerativeUIProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ninterface Haiku {\n japanese: string[];\n english: string[];\n image_name: string | null;\n gradient: string;\n}\n\nexport default function ToolBasedGenerativeUI({ params }: ToolBasedGenerativeUIProps) {\n const { integrationId } = React.use(params);\n const { chatDefaultOpen } = useURLParams();\n\n return (\n \n \n \n \n );\n}\n\nfunction SidebarWithSuggestions({ defaultOpen }: { defaultOpen: boolean }) {\n useConfigureSuggestions({\n suggestions: [\n { title: \"Nature Haiku\", message: \"Write me a haiku about nature.\" },\n { title: \"Ocean Haiku\", message: \"Create a haiku about the ocean.\" },\n { title: \"Spring Haiku\", message: \"Generate a haiku about spring.\" },\n ],\n available: \"always\",\n });\n\n return (\n \n );\n}\n\nconst VALID_IMAGE_NAMES = [\n \"Osaka_Castle_Turret_Stone_Wall_Pine_Trees_Daytime.jpg\",\n \"Tokyo_Skyline_Night_Tokyo_Tower_Mount_Fuji_View.jpg\",\n \"Itsukushima_Shrine_Miyajima_Floating_Torii_Gate_Sunset_Long_Exposure.jpg\",\n \"Takachiho_Gorge_Waterfall_River_Lush_Greenery_Japan.jpg\",\n \"Bonsai_Tree_Potted_Japanese_Art_Green_Foliage.jpeg\",\n \"Shirakawa-go_Gassho-zukuri_Thatched_Roof_Village_Aerial_View.jpg\",\n \"Ginkaku-ji_Silver_Pavilion_Kyoto_Japanese_Garden_Pond_Reflection.jpg\",\n \"Senso-ji_Temple_Asakusa_Cherry_Blossoms_Kimono_Umbrella.jpg\",\n \"Cherry_Blossoms_Sakura_Night_View_City_Lights_Japan.jpg\",\n \"Mount_Fuji_Lake_Reflection_Cherry_Blossoms_Sakura_Spring.jpg\",\n];\n\nfunction HaikuDisplay() {\n const [activeIndex, setActiveIndex] = useState(0);\n const [haikus, setHaikus] = useState([\n {\n japanese: [\"仮の句よ\", \"まっさらながら\", \"花を呼ぶ\"],\n english: [\"A placeholder verse—\", \"even in a blank canvas,\", \"it beckons flowers.\"],\n image_name: null,\n gradient: \"\",\n },\n ]);\n\n useFrontendTool(\n {\n agentId: \"tool_based_generative_ui\",\n name: \"generate_haiku\",\n parameters: z.object({\n japanese: z.array(z.string()).describe(\"3 lines of haiku in Japanese\"),\n english: z.array(z.string()).describe(\"3 lines of haiku translated to English\"),\n image_name: z.string().describe(`One relevant image name from: ${VALID_IMAGE_NAMES.join(\", \")}`),\n gradient: z.string().describe(\"CSS Gradient color for the background\"),\n }) ,\n followUp: false,\n handler: async ({ japanese, english, image_name, gradient }: { japanese: string[]; english: string[]; image_name: string; gradient: string }) => {\n const newHaiku: Haiku = {\n japanese: japanese || [],\n english: english || [],\n image_name: image_name || null,\n gradient: gradient || \"\",\n };\n setHaikus((prev) => [\n newHaiku,\n ...prev.filter((h) => h.english[0] !== \"A placeholder verse—\"),\n ]);\n setActiveIndex(0);\n return \"Haiku generated!\";\n },\n render: ({ args }: { args: Partial }) => {\n if (!args.japanese) return <>;\n return ;\n },\n },\n [haikus],\n );\n\n const currentHaiku = haikus[activeIndex];\n\n return (\n
\n
\n \n \n {haikus.map((haiku, index) => (\n \n \n \n ))}\n \n {haikus.length > 1 && (\n <>\n \n \n \n )}\n \n
\n
\n );\n}\n\nfunction HaikuCard({ haiku }: { haiku: Partial }) {\n return (\n \n {/* Decorative background elements */}\n
\n
\n\n {/* Haiku Text */}\n
\n {haiku.japanese?.map((line, index) => (\n \n \n {line}\n

\n \n {haiku.english?.[index]}\n

\n
\n ))}\n
\n\n {/* Image */}\n {haiku.image_name && (\n
\n
\n \n
\n
\n
\n )}\n
\n );\n}\n", "language": "typescript", "type": "file" }, { "name": "style.css", - "content": ".copilotKitInput {\n border-bottom-left-radius: 0.75rem;\n border-bottom-right-radius: 0.75rem;\n border-top-left-radius: 0.75rem;\n border-top-right-radius: 0.75rem;\n border: 1px solid var(--copilot-kit-separator-color) !important;\n}\n\n.copilotKitChat {\n background-color: #fff !important;\n}\n", + "content": ".page-background {\n /* Darker gradient background */\n background: linear-gradient(170deg, #e9ecef 0%, #ced4da 100%);\n}\n\n@keyframes fade-scale-in {\n from {\n opacity: 0;\n transform: translateY(10px) scale(0.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n/* Updated card entry animation */\n@keyframes pop-in {\n 0% {\n opacity: 0;\n transform: translateY(15px) scale(0.95);\n }\n 70% {\n opacity: 1;\n transform: translateY(-2px) scale(1.02);\n }\n 100% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n/* Animation for subtle background gradient movement */\n@keyframes animated-gradient {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n\n/* Animation for flash effect on apply */\n@keyframes flash-border-glow {\n 0% {\n /* Start slightly intensified */\n border-top-color: #ff5b4a !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 25px rgba(255, 91, 74, 0.5);\n }\n 50% {\n /* Peak intensity */\n border-top-color: #ff4733 !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 35px rgba(255, 71, 51, 0.7);\n }\n 100% {\n /* Return to default state appearance */\n border-top-color: #ff6f61 !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 10px rgba(255, 111, 97, 0.15);\n }\n}\n\n/* Existing animation for haiku lines */\n@keyframes fade-slide-in {\n from {\n opacity: 0;\n transform: translateX(-15px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n.animated-fade-in {\n /* Use the new pop-in animation */\n animation: pop-in 0.6s ease-out forwards;\n}\n\n.haiku-card {\n /* Subtle animated gradient background */\n background: linear-gradient(120deg, #ffffff 0%, #fdfdfd 50%, #ffffff 100%);\n background-size: 200% 200%;\n animation: animated-gradient 10s ease infinite;\n\n /* === Explicit Border Override Attempt === */\n /* 1. Set the default grey border for all sides */\n border: 1px solid #dee2e6;\n\n /* 2. Explicitly override the top border immediately after */\n border-top: 10px solid #ff6f61 !important; /* Orange top - Added !important */\n /* === End Explicit Border Override Attempt === */\n\n padding: 2.5rem 3rem;\n border-radius: 20px;\n\n /* Default glow intensity */\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 15px rgba(255, 111, 97, 0.25);\n text-align: left;\n max-width: 745px;\n margin: 3rem auto;\n min-width: 600px;\n\n /* Transition */\n transition: transform 0.35s ease, box-shadow 0.35s ease, border-top-width 0.35s ease, border-top-color 0.35s ease;\n}\n\n.haiku-card:hover {\n transform: translateY(-8px) scale(1.03);\n /* Enhanced shadow + Glow */\n box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 25px rgba(255, 91, 74, 0.5);\n /* Modify only top border properties */\n border-top-width: 14px !important; /* Added !important */\n border-top-color: #ff5b4a !important; /* Added !important */\n}\n\n.haiku-card .flex {\n margin-bottom: 1.5rem;\n}\n\n.haiku-card .flex.haiku-line { /* Target the lines specifically */\n margin-bottom: 1.5rem;\n opacity: 0; /* Start hidden for animation */\n animation: fade-slide-in 0.5s ease-out forwards;\n /* animation-delay is set inline in page.tsx */\n}\n\n/* Remove previous explicit color overrides - rely on Tailwind */\n/* .haiku-card p.text-4xl {\n color: #212529;\n}\n\n.haiku-card p.text-base {\n color: #495057;\n} */\n\n.haiku-card.applied-flash {\n /* Apply the flash animation once */\n /* Note: animation itself has !important on border-top-color */\n animation: flash-border-glow 0.6s ease-out forwards;\n}\n\n/* Styling for images within the main haiku card */\n.haiku-card-image {\n width: 9.5rem; /* Increased size (approx w-48) */\n height: 9.5rem; /* Increased size (approx h-48) */\n object-fit: cover;\n border-radius: 1.5rem; /* rounded-xl */\n border: 1px solid #e5e7eb;\n /* Enhanced shadow with subtle orange hint */\n box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1),\n 0 3px 6px rgba(0, 0, 0, 0.08),\n 0 0 10px rgba(255, 111, 97, 0.2);\n /* Inherit animation delay from inline style */\n animation-name: fadeIn;\n animation-duration: 0.5s;\n animation-fill-mode: both;\n}\n\n/* Styling for images within the suggestion card */\n.suggestion-card-image {\n width: 6.5rem; /* Increased slightly (w-20) */\n height: 6.5rem; /* Increased slightly (h-20) */\n object-fit: cover;\n border-radius: 1rem; /* Equivalent to rounded-md */\n border: 1px solid #d1d5db; /* Equivalent to border (using Tailwind gray-300) */\n margin-top: 0.5rem;\n /* Added shadow for suggestion images */\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1),\n 0 2px 4px rgba(0, 0, 0, 0.06);\n transition: all 0.2s ease-in-out; /* Added for smooth deselection */\n}\n\n/* Styling for the focused suggestion card image */\n.suggestion-card-image-focus {\n width: 6.5rem;\n height: 6.5rem;\n object-fit: cover;\n border-radius: 1rem;\n margin-top: 0.5rem;\n /* Highlight styles */\n border: 2px solid #ff6f61; /* Thicker, themed border */\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), /* Base shadow for depth */\n 0 0 12px rgba(255, 111, 97, 0.6); /* Orange glow */\n transform: scale(1.05); /* Slightly scale up */\n transition: all 0.2s ease-in-out; /* Smooth transition for focus */\n}\n\n/* Styling for the suggestion card container in the sidebar */\n.suggestion-card {\n border: 1px solid #dee2e6; /* Same default border as haiku-card */\n border-top: 10px solid #ff6f61; /* Same orange top border */\n border-radius: 0.375rem; /* Default rounded-md */\n /* Note: background-color is set by Tailwind bg-gray-100 */\n /* Other styles like padding, margin, flex are handled by Tailwind */\n}\n\n.suggestion-image-container {\n display: flex;\n gap: 1rem;\n justify-content: space-between;\n width: 100%;\n height: 6.5rem;\n}\n\n/* Mobile responsive styles - matches useMobileView hook breakpoint */\n@media (max-width: 767px) {\n .haiku-card {\n padding: 1rem 1.5rem; /* Reduced from 2.5rem 3rem */\n min-width: auto; /* Remove min-width constraint */\n max-width: 100%; /* Full width on mobile */\n margin: 1rem auto; /* Reduced margin */\n }\n\n .haiku-card-image {\n width: 5.625rem; /* 90px - smaller on mobile */\n height: 5.625rem; /* 90px - smaller on mobile */\n }\n\n .suggestion-card-image {\n width: 5rem; /* Slightly smaller on mobile */\n height: 5rem; /* Slightly smaller on mobile */\n }\n\n .suggestion-card-image-focus {\n width: 5rem; /* Slightly smaller on mobile */\n height: 5rem; /* Slightly smaller on mobile */\n }\n}\n", "language": "css", "type": "file" }, { "name": "README.mdx", - "content": "# 🚀 Agentic Generative UI Task Executor\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic generative UI** capabilities:\n\n1. **Real-time Status Updates**: The Copilot provides live feedback as it works\n through complex tasks\n2. **Long-running Task Execution**: See how agents can handle extended processes\n with continuous feedback\n3. **Dynamic UI Generation**: The interface updates in real-time to reflect the\n agent's progress\n\n## How to Interact\n\nSimply ask your Copilot to perform any moderately complex task:\n\n- \"Make me a sandwich\"\n- \"Plan a vacation to Japan\"\n- \"Create a weekly workout routine\"\n\nThe Copilot will break down the task into steps and begin \"executing\" them,\nproviding real-time status updates as it progresses.\n\n## ✨ Agentic Generative UI in Action\n\n**What's happening technically:**\n\n- The agent analyzes your request and creates a detailed execution plan\n- Each step is processed sequentially with realistic timing\n- Status updates are streamed to the frontend using CopilotKit's streaming\n capabilities\n- The UI dynamically renders these updates without page refreshes\n- The entire flow is managed by the agent, requiring no manual intervention\n\n**What you'll see in this demo:**\n\n- The Copilot breaks your task into logical steps\n- A status indicator shows the current progress\n- Each step is highlighted as it's being executed\n- Detailed status messages explain what's happening at each moment\n- Upon completion, you receive a summary of the task execution\n\nThis pattern of providing real-time progress for long-running tasks is perfect\nfor scenarios where users benefit from transparency into complex processes -\nfrom data analysis to content creation, system configurations, or multi-stage\nworkflows!\n", + "content": "# 🪶 Tool-Based Generative UI Haiku Creator\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **tool-based generative UI** capabilities:\n\n1. **Frontend Rendering of Tool Calls**: Backend tool calls are automatically\n rendered in the UI\n2. **Dynamic UI Generation**: The UI updates in real-time as the agent generates\n content\n3. **Elegant Content Presentation**: Complex structured data (haikus) are\n beautifully displayed\n\n## How to Interact\n\nChat with your Copilot and ask for haikus about different topics:\n\n- \"Create a haiku about nature\"\n- \"Write a haiku about technology\"\n- \"Generate a haiku about the changing seasons\"\n- \"Make a humorous haiku about programming\"\n\nEach request will trigger the agent to generate a haiku and display it in a\nvisually appealing card format in the UI.\n\n## ✨ Tool-Based Generative UI in Action\n\n**What's happening technically:**\n\n- The agent processes your request and determines it should create a haiku\n- It calls a backend tool that returns structured haiku data\n- CopilotKit automatically renders this tool call in the frontend\n- The rendering is handled by the registered tool component in your React app\n- No manual state management is required to display the results\n\n**What you'll see in this demo:**\n\n- As you request a haiku, a beautifully formatted card appears in the UI\n- The haiku follows the traditional 5-7-5 syllable structure\n- Each haiku is presented with consistent styling\n- Multiple haikus can be generated in sequence\n- The UI adapts to display each new piece of content\n\nThis pattern of tool-based generative UI can be extended to create any kind of\ndynamic content - from data visualizations to interactive components, all driven\nby your Copilot's tool calls!\n", "language": "markdown", "type": "file" }, { - "name": "agentic_generative_ui.py", - "content": "\"\"\"Agentic Generative UI example for Langroid.\n\nThis example demonstrates dynamic UI generation using AG-UI state events.\nThe agent creates plans with steps and updates their status dynamically.\n\"\"\"\nimport json\nimport os\nfrom pathlib import Path\nfrom textwrap import dedent\nfrom typing import Any, Literal, Optional\nfrom dotenv import load_dotenv\n\nenv_path = Path(__file__).parent.parent.parent / '.env'\nload_dotenv(dotenv_path=env_path)\n\nimport langroid as lr\nfrom langroid.agent import ToolMessage, ChatAgent\nfrom langroid.language_models import OpenAIChatModel\nfrom pydantic import BaseModel, Field\nfrom ag_ui.core import EventType, StateSnapshotEvent, StateDeltaEvent\nfrom ag_ui_langroid import LangroidAgent, create_langroid_app\n\nStepStatus = Literal['pending', 'completed']\n\n\nclass Step(BaseModel):\n \"\"\"Represents a step in a plan.\"\"\"\n\n description: str = Field(description='The description of the step')\n status: StepStatus = Field(\n default='pending',\n description='The status of the step (e.g., pending, completed)',\n )\n\n\nclass Plan(BaseModel):\n \"\"\"Represents a plan with multiple steps.\"\"\"\n\n steps: list[Step] = Field(default_factory=list, description='The steps in the plan')\n\n\nclass JSONPatchOp(BaseModel):\n \"\"\"A class representing a JSON Patch operation (RFC 6902).\"\"\"\n\n op: Literal['add', 'remove', 'replace', 'move', 'copy', 'test'] = Field(\n description='The operation to perform: add, remove, replace, move, copy, or test',\n )\n path: str = Field(description='JSON Pointer (RFC 6901) to the target location')\n value: Any = Field(\n default=None,\n description='The value to apply (for add, replace operations)',\n )\n from_: str | None = Field(\n default=None,\n alias='from',\n description='Source path (for move, copy operations)',\n )\n\n\nclass CreatePlanTool(ToolMessage):\n \"\"\"Create a plan with multiple steps.\"\"\"\n request: str = \"create_plan\"\n purpose: str = \"\"\"\n Create a plan with multiple steps.\n Use this when the user asks you to create a plan or break down a task into steps.\n This sets the initial state of the steps.\n \"\"\"\n steps: list[str]\n\n\nclass UpdatePlanStepTool(ToolMessage):\n \"\"\"Update the status or description of a step in the plan.\"\"\"\n request: str = \"update_plan_step\"\n purpose: str = \"\"\"\n Update the status or description of a specific step in the plan.\n Use this to mark steps as completed or update their descriptions.\n The index is 0-based.\n \"\"\"\n index: int\n description: Optional[str] = None\n status: Optional[StepStatus] = None\n\n\n# Configure LLM\nllm_config = lr.language_models.OpenAIGPTConfig(\n chat_model=OpenAIChatModel.GPT4_1_MINI,\n api_key=os.getenv(\"OPENAI_API_KEY\"),\n # Make behavior deterministic for demos and e2e tests\n temperature=0.0,\n)\n\nagent_config = lr.ChatAgentConfig(\n name=\"PlanAssistant\",\n llm=llm_config,\n system_message=dedent(\"\"\"\n You are a helpful assistant that can create plans with multiple steps.\n\n CRITICAL RULES - YOU MUST FOLLOW THESE EXACTLY:\n 1. When the user asks you to create a plan, make a plan, or break down a task into steps, you MUST IMMEDIATELY call the `create_plan` tool. Do NOT respond with text first.\n 2. NEVER say you have \"already created\" a plan unless you have actually called the `create_plan` tool in this conversation.\n 3. NEVER describe steps in your text response - the `create_plan` tool will handle displaying the steps.\n 4. The `create_plan` tool requires a `steps` parameter which is a list of step descriptions as strings.\n 5. After calling `create_plan`, provide a brief summary (1-2 sentences with emojis) of what you did.\n 6. Use `update_plan_step` ONLY when the user explicitly asks to modify an existing plan's steps.\n \n Examples:\n - User: \"give me a plan to make brownies\" → You MUST call create_plan with steps like [\"Gather ingredients\", \"Mix batter\", \"Bake\", etc.]\n - User: \"Go to Mars\" → You MUST call create_plan with steps for a Mars mission\n - User: \"mark step 3 as complete\" → Use update_plan_step to update the status\n \"\"\"),\n use_tools=True,\n use_functions_api=True,\n)\n\n\nclass PlanAssistantAgent(ChatAgent):\n \"\"\"ChatAgent with plan management tool handlers that return AG-UI events.\"\"\"\n \n def __init__(self, config):\n super().__init__(config)\n self._plan_data = None\n self._last_step_update = None\n \n def create_plan(self, msg: CreatePlanTool) -> str:\n \"\"\"\n Handle create_plan tool execution.\n Creates plan and returns result. State events will be handled by handler method.\n Returns string result for Langroid to continue processing.\n Note: Don't include steps in the return value - the handler will emit them via STATE_SNAPSHOT.\n This prevents the frontend from creating a duplicate component from the tool result.\n \"\"\"\n plan = Plan(\n steps=[Step(description=step) for step in msg.steps],\n )\n self._plan_data = plan.model_dump()\n # Return simple confirmation without steps - handler will emit STATE_SNAPSHOT\n # This matches LangGraph's pattern of returning \"Steps executed.\" without the steps\n return json.dumps({\"status\": \"plan_created\", \"steps_count\": len(msg.steps)})\n \n def update_plan_step(\n self, msg: UpdatePlanStepTool\n ) -> str:\n \"\"\"\n Handle update_plan_step tool execution.\n Updates step and returns result. State events will be handled by handler method.\n Returns string result for Langroid to continue processing.\n \"\"\"\n self._last_step_update = {\n \"index\": msg.index,\n \"description\": msg.description,\n \"status\": msg.status\n }\n status_msg = f\"updated step {msg.index}\"\n if msg.status:\n status_msg += f\" to {msg.status}\"\n return json.dumps({\"status\": \"step_updated\", \"index\": msg.index, \"message\": status_msg})\n \n async def _handle_create_plan_result(self, result_data: dict):\n \"\"\"\n Handler for create_plan tool result - emits state events.\n Automatically processes all steps and emits state deltas.\n Uses self._plan_data which was set during create_plan execution.\n \"\"\"\n import asyncio\n import random\n \n # Get steps from _plan_data (set during create_plan) instead of result_data\n # This allows us to return a simple tool result without steps to prevent duplicate components\n if not hasattr(self, \"_plan_data\") or not self._plan_data:\n return\n \n steps = self._plan_data.get(\"steps\", [])\n if not steps:\n return\n \n working_steps = []\n for step in steps:\n if isinstance(step, dict):\n step_dict = dict(step)\n if \"status\" not in step_dict:\n step_dict[\"status\"] = \"pending\"\n working_steps.append(step_dict)\n else:\n working_steps.append({\"description\": str(step), \"status\": \"pending\"})\n \n yield StateSnapshotEvent(\n type=EventType.STATE_SNAPSHOT,\n snapshot={\"steps\": working_steps},\n )\n \n for index, _ in enumerate(working_steps):\n await asyncio.sleep(random.uniform(0.3, 0.8))\n working_steps[index][\"status\"] = \"in_progress\"\n yield StateDeltaEvent(\n type=EventType.STATE_DELTA,\n delta=[\n {\n \"op\": \"replace\",\n \"path\": f\"/steps/{index}/status\",\n \"value\": \"in_progress\",\n }\n ],\n )\n \n await asyncio.sleep(random.uniform(0.4, 1.0))\n working_steps[index][\"status\"] = \"completed\"\n yield StateDeltaEvent(\n type=EventType.STATE_DELTA,\n delta=[\n {\n \"op\": \"replace\",\n \"path\": f\"/steps/{index}/status\",\n \"value\": \"completed\",\n }\n ],\n )\n \n yield StateSnapshotEvent(\n type=EventType.STATE_SNAPSHOT,\n snapshot={\"steps\": working_steps},\n )\n \n async def _handle_update_plan_step_result(self, result_data: dict):\n \"\"\"\n Handler for update_plan_step tool result - emits state delta event.\n \"\"\"\n if not hasattr(self, \"_last_step_update\"):\n return\n \n update = self._last_step_update\n changes = []\n \n if update.get(\"description\") is not None:\n changes.append({\n \"op\": \"replace\",\n \"path\": f\"/steps/{update['index']}/description\",\n \"value\": update[\"description\"],\n })\n if update.get(\"status\") is not None:\n changes.append({\n \"op\": \"replace\",\n \"path\": f\"/steps/{update['index']}/status\",\n \"value\": update[\"status\"],\n })\n \n if changes:\n yield StateDeltaEvent(\n type=EventType.STATE_DELTA,\n delta=changes,\n )\n\n\nchat_agent = PlanAssistantAgent(agent_config)\nchat_agent.enable_message(CreatePlanTool)\nchat_agent.enable_message(UpdatePlanStepTool)\n\ntask = lr.Task(\n chat_agent,\n name=\"PlanAssistant\",\n interactive=False,\n single_round=False,\n)\n\nagui_agent = LangroidAgent(\n agent=task,\n name=\"agentic_generative_ui\",\n description=\"Langroid agent with agentic generative UI support - dynamic plan creation and step updates\",\n)\n\napp = create_langroid_app(agui_agent, \"/\")\n\n", + "name": "tool_based_generative_ui.py", + "content": "\"\"\"Tool-based generative UI — frontend tool renders the content.\n\nLike :mod:`human_in_the_loop`, the tool (``generate_haiku``) is *client*-owned:\nit arrives per-request in ``RunAgentInput.tools``, gets wrapped into an SDK\n``FunctionTool`` proxy, and ``StopAtTools`` ends the run the moment the model\ncalls it. The difference is intent: here the tool call *is* the deliverable —\nthe frontend renders the haiku card from the streamed ``TOOL_CALL_ARGS``,\nno approval round-trip expected.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, StopAtTools\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\n# Must match the AG-UI client tool name the frontend declares in\n# RunAgentInput.tools for this demo.\nFRONTEND_TOOL_NAME = \"generate_haiku\"\n\nINSTRUCTIONS = \"\"\"You are a creative writing assistant that renders haikus with a UI component.\n\nWhen the user asks for a haiku:\n1. Immediately call the `generate_haiku` tool with the haiku data\n (Japanese lines, English lines, and any other fields the tool declares).\n2. Do NOT write the haiku as plain text — the frontend renders it.\n\nFor non-creative requests, respond normally without the tool.\n\"\"\"\n\n\ndef create_tool_based_generative_ui_agent() -> Agent:\n return Agent(\n name=\"haiku_assistant\",\n model=DEFAULT_MODEL,\n instructions=INSTRUCTIONS,\n tool_use_behavior=StopAtTools(stop_at_tool_names=[FRONTEND_TOOL_NAME]),\n )\n\n\nagent = OpenAIAgentsAgent(\n create_tool_based_generative_ui_agent(), name=\"tool_based_generative_ui\"\n)\napp = FastAPI(title=\"Tool-based generative UI AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } ], - "langroid::shared_state": [ + "openai-agents-python::subagents": [ { "name": "page.tsx", - "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n
\n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n const changedKeysRef = useRef([]);\n\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as any)[key] !== undefined &&\n (agentState.recipe as any)[key] !== null\n ) {\n let agentValue = (agentState.recipe as any)[key];\n const recipeValue = (recipe as any)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as any)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "content": "\"use client\";\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useConfigureSuggestions, useRenderTool } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { z } from \"zod\";\n\ninterface SubagentsProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ntype Role = \"research\" | \"writer\" | \"critic\";\ntype Status = \"inProgress\" | \"executing\" | \"complete\";\n\nconst ROLES: { role: Role; label: string; emoji: string }[] = [\n { role: \"research\", label: \"Researcher\", emoji: \"🔎\" },\n { role: \"writer\", label: \"Writer\", emoji: \"✍️\" },\n { role: \"critic\", label: \"Critic\", emoji: \"🧐\" },\n];\n\ninterface DelegationEntry {\n id: string;\n role: Role;\n status: Status;\n text: string;\n}\n\nconst Subagents: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst SubagentsView = () => {\n const [active, setActive] = useState>({\n research: null,\n writer: null,\n critic: null,\n });\n const [log, setLog] = useState([]);\n\n // Called by each DelegationTracker instance as its tool call's status\n // changes. Same shared state feeds both the role chips (who's working\n // right now) and the log (what happened, in order).\n const track = useCallback((entry: DelegationEntry) => {\n setActive((prev) => ({\n ...prev,\n [entry.role]: entry.status === \"complete\" ? null : entry.status,\n }));\n setLog((prev) => {\n const idx = prev.findIndex((e) => e.id === entry.id);\n if (idx === -1) return [...prev, entry];\n const next = [...prev];\n next[idx] = entry;\n return next;\n });\n }, []);\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"History of AI\",\n message: \"Write a short article about the history of AI\",\n },\n {\n title: \"History of programming languages\",\n message: \"Write a short piece about the history of programming languages\",\n },\n ],\n available: \"always\",\n });\n\n useRenderTool({\n name: \"research_topic\",\n agentId: \"subagents\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"write_prose\",\n agentId: \"subagents\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n useRenderTool({\n name: \"critique_draft\",\n agentId: \"subagents\",\n parameters: z.object({ input: z.string().optional() }),\n render: ({ toolCallId, status, args, result }: any) => (\n \n ),\n });\n\n return (\n
\n
\n

Supervisor's team

\n
\n {ROLES.map(({ role, label, emoji }) => {\n const status = active[role];\n return (\n \n {emoji}\n {label}\n {status && (\n \n {status === \"executing\" ? \"working\" : \"starting\"}\n \n )}\n
\n );\n })}\n
\n\n

\n Delegation log\n

\n
\n {log.length === 0 && (\n
No delegations yet.
\n )}\n {log.map((entry) => {\n const roleInfo = ROLES.find((r) => r.role === entry.role);\n return (\n \n
\n \n {roleInfo?.emoji} {roleInfo?.label}\n \n \n {entry.status}\n \n
\n {entry.text && (\n
\n {entry.text}\n
\n )}\n
\n );\n })}\n
\n \n\n
\n
\n \n
\n
\n \n );\n};\n\n// Each tool call renders its own instance of this. `render` is invoked as a\n// real component (not a plain function), so effects are legal here — this\n// is what reports status upstream into the shared chip/log state on every\n// inProgress → executing → complete transition, in addition to rendering\n// its own inline card in the chat transcript.\nfunction DelegationTracker({\n role,\n toolCallId,\n status,\n preview,\n result,\n onUpdate,\n}: {\n role: Role;\n toolCallId: string;\n status: Status;\n preview?: string;\n result?: string;\n onUpdate: (entry: DelegationEntry) => void;\n}) {\n const text = status === \"complete\" ? (result ?? \"\") : (preview ?? \"\");\n\n useEffect(() => {\n onUpdate({ id: toolCallId, role, status, text });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [toolCallId, role, status, text]);\n\n const roleInfo = ROLES.find((r) => r.role === role)!;\n const label =\n status === \"complete\"\n ? `${roleInfo.label} finished`\n : status === \"executing\"\n ? `${roleInfo.label} working…`\n : `Calling ${roleInfo.label.toLowerCase()}…`;\n\n return (\n
\n {roleInfo.emoji} {label}\n
\n );\n}\n\nexport default Subagents;\n", "language": "typescript", "type": "file" }, - { - "name": "style.css", - "content": "/* Recipe App Styles */\n.app-container {\n min-height: 100vh;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n background-attachment: fixed;\n position: relative;\n overflow: auto;\n}\n\n.recipe-card {\n background-color: rgba(255, 255, 255, 0.97);\n border-radius: 16px;\n box-shadow: 0 15px 30px rgba(0, 0, 0, 0.25), 0 5px 15px rgba(0, 0, 0, 0.15);\n width: 100%;\n max-width: 750px;\n margin: 20px auto;\n padding: 14px 32px;\n position: relative;\n z-index: 1;\n backdrop-filter: blur(5px);\n border: 1px solid rgba(255, 255, 255, 0.3);\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n animation: fadeIn 0.5s ease-out forwards;\n box-sizing: border-box;\n overflow: hidden;\n}\n\n.recipe-card:hover {\n transform: translateY(-5px);\n box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 10px 20px rgba(0, 0, 0, 0.2);\n}\n\n/* Recipe Header */\n.recipe-header {\n margin-bottom: 24px;\n}\n\n.recipe-title-input {\n width: 100%;\n font-size: 24px;\n font-weight: bold;\n border: none;\n outline: none;\n padding: 8px 0;\n margin-bottom: 0px;\n}\n\n.recipe-meta {\n display: flex;\n align-items: center;\n gap: 20px;\n margin-top: 5px;\n margin-bottom: 14px;\n}\n\n.meta-item {\n display: flex;\n align-items: center;\n gap: 8px;\n color: #555;\n}\n\n.meta-icon {\n font-size: 20px;\n color: #777;\n}\n\n.meta-text {\n font-size: 15px;\n}\n\n/* Recipe Meta Selects */\n.meta-item select {\n border: none;\n background: transparent;\n font-size: 15px;\n color: #555;\n cursor: pointer;\n outline: none;\n padding-right: 18px;\n transition: color 0.2s, transform 0.1s;\n font-weight: 500;\n}\n\n.meta-item select:hover,\n.meta-item select:focus {\n color: #FF5722;\n}\n\n.meta-item select:active {\n transform: scale(0.98);\n}\n\n.meta-item select option {\n color: #333;\n background-color: white;\n font-weight: normal;\n padding: 8px;\n}\n\n/* Section Container */\n.section-container {\n margin-bottom: 20px;\n position: relative;\n width: 100%;\n}\n\n.section-title {\n font-size: 20px;\n font-weight: 700;\n margin-bottom: 20px;\n color: #333;\n position: relative;\n display: inline-block;\n}\n\n.section-title:after {\n content: \"\";\n position: absolute;\n bottom: -8px;\n left: 0;\n width: 40px;\n height: 3px;\n background-color: #ff7043;\n border-radius: 3px;\n}\n\n/* Dietary Preferences */\n.dietary-options {\n display: flex;\n flex-wrap: wrap;\n gap: 10px 16px;\n margin-bottom: 16px;\n width: 100%;\n}\n\n.dietary-option {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 14px;\n cursor: pointer;\n margin-bottom: 4px;\n}\n\n.dietary-option input {\n cursor: pointer;\n}\n\n/* Ingredients */\n.ingredients-container {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-bottom: 15px;\n width: 100%;\n box-sizing: border-box;\n}\n\n.ingredient-card {\n display: flex;\n align-items: center;\n background-color: rgba(255, 255, 255, 0.9);\n border-radius: 12px;\n padding: 12px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);\n position: relative;\n transition: all 0.2s ease;\n border: 1px solid rgba(240, 240, 240, 0.8);\n width: calc(33.333% - 7px);\n box-sizing: border-box;\n}\n\n.ingredient-card:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);\n}\n\n.ingredient-card .remove-button {\n position: absolute;\n right: 10px;\n top: 10px;\n background: none;\n border: none;\n color: #ccc;\n font-size: 16px;\n cursor: pointer;\n display: none;\n padding: 0;\n width: 24px;\n height: 24px;\n line-height: 1;\n}\n\n.ingredient-card:hover .remove-button {\n display: block;\n}\n\n.ingredient-icon {\n font-size: 24px;\n margin-right: 12px;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n background-color: #f7f7f7;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.ingredient-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 3px;\n min-width: 0;\n}\n\n.ingredient-name-input,\n.ingredient-amount-input {\n border: none;\n background: transparent;\n outline: none;\n width: 100%;\n padding: 0;\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n}\n\n.ingredient-name-input {\n font-weight: 500;\n font-size: 14px;\n}\n\n.ingredient-amount-input {\n font-size: 13px;\n color: #666;\n}\n\n.ingredient-name-input::placeholder,\n.ingredient-amount-input::placeholder {\n color: #aaa;\n}\n\n.remove-button {\n background: none;\n border: none;\n color: #999;\n font-size: 20px;\n cursor: pointer;\n padding: 0;\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 10px;\n}\n\n.remove-button:hover {\n color: #FF5722;\n}\n\n/* Instructions */\n.instructions-container {\n display: flex;\n flex-direction: column;\n gap: 6px;\n position: relative;\n margin-bottom: 12px;\n width: 100%;\n}\n\n.instruction-item {\n position: relative;\n display: flex;\n width: 100%;\n box-sizing: border-box;\n margin-bottom: 8px;\n align-items: flex-start;\n}\n\n.instruction-number {\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 26px;\n height: 26px;\n background-color: #ff7043;\n color: white;\n border-radius: 50%;\n font-weight: 600;\n flex-shrink: 0;\n box-shadow: 0 2px 4px rgba(255, 112, 67, 0.3);\n z-index: 1;\n font-size: 13px;\n margin-top: 2px;\n}\n\n.instruction-line {\n position: absolute;\n left: 13px; /* Half of the number circle width */\n top: 22px;\n bottom: -18px;\n width: 2px;\n background: linear-gradient(to bottom, #ff7043 60%, rgba(255, 112, 67, 0.4));\n z-index: 0;\n}\n\n.instruction-content {\n background-color: white;\n border-radius: 10px;\n padding: 10px 14px;\n margin-left: 12px;\n flex-grow: 1;\n transition: all 0.2s ease;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);\n border: 1px solid rgba(240, 240, 240, 0.8);\n position: relative;\n width: calc(100% - 38px);\n box-sizing: border-box;\n display: flex;\n align-items: center;\n}\n\n.instruction-content-editing {\n background-color: #fff9f6;\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12), 0 0 0 2px rgba(255, 112, 67, 0.2);\n}\n\n.instruction-content:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);\n}\n\n.instruction-textarea {\n width: 100%;\n background: transparent;\n border: none;\n resize: vertical;\n font-family: inherit;\n font-size: 14px;\n line-height: 1.4;\n min-height: 20px;\n outline: none;\n padding: 0;\n margin: 0;\n}\n\n.instruction-delete-btn {\n position: absolute;\n background: none;\n border: none;\n color: #ccc;\n font-size: 16px;\n cursor: pointer;\n display: none;\n padding: 0;\n width: 20px;\n height: 20px;\n line-height: 1;\n top: 50%;\n transform: translateY(-50%);\n right: 8px;\n}\n\n.instruction-content:hover .instruction-delete-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Action Button */\n.action-container {\n display: flex;\n justify-content: center;\n margin-top: 40px;\n padding-bottom: 20px;\n position: relative;\n}\n\n.improve-button {\n background-color: #ff7043;\n border: none;\n color: white;\n border-radius: 30px;\n font-size: 18px;\n font-weight: 600;\n padding: 14px 28px;\n cursor: pointer;\n transition: all 0.3s ease;\n box-shadow: 0 4px 15px rgba(255, 112, 67, 0.4);\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n position: relative;\n min-width: 180px;\n}\n\n.improve-button:hover {\n background-color: #ff5722;\n transform: translateY(-2px);\n box-shadow: 0 8px 20px rgba(255, 112, 67, 0.5);\n}\n\n.improve-button.loading {\n background-color: #ff7043;\n opacity: 0.8;\n cursor: not-allowed;\n padding-left: 42px; /* Reduced padding to bring text closer to icon */\n padding-right: 22px; /* Balance the button */\n justify-content: flex-start; /* Left align text for better alignment with icon */\n}\n\n.improve-button.loading:after {\n content: \"\"; /* Add space between icon and text */\n display: inline-block;\n width: 8px; /* Width of the space */\n}\n\n.improve-button:before {\n content: \"\";\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83'/%3E%3C/svg%3E\");\n width: 20px; /* Slightly smaller icon */\n height: 20px;\n background-repeat: no-repeat;\n background-size: contain;\n position: absolute;\n left: 16px; /* Slightly adjusted */\n top: 50%;\n transform: translateY(-50%);\n display: none;\n}\n\n.improve-button.loading:before {\n display: block;\n animation: spin 1.5s linear infinite;\n}\n\n@keyframes spin {\n 0% { transform: translateY(-50%) rotate(0deg); }\n 100% { transform: translateY(-50%) rotate(360deg); }\n}\n\n/* Ping Animation */\n.ping-animation {\n position: absolute;\n display: flex;\n width: 12px;\n height: 12px;\n top: 0;\n right: 0;\n}\n\n.ping-circle {\n position: absolute;\n display: inline-flex;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n background-color: #38BDF8;\n opacity: 0.75;\n animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;\n}\n\n.ping-dot {\n position: relative;\n display: inline-flex;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background-color: #0EA5E9;\n}\n\n@keyframes ping {\n 75%, 100% {\n transform: scale(2);\n opacity: 0;\n }\n}\n\n/* Instruction hover effects */\n.instruction-item:hover .instruction-delete-btn {\n display: flex !important;\n}\n\n/* Add some subtle animations */\n@keyframes fadeIn {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* Better center alignment for the recipe card */\n.recipe-card-container {\n display: flex;\n justify-content: center;\n width: 100%;\n position: relative;\n z-index: 1;\n margin: 0 auto;\n box-sizing: border-box;\n}\n\n/* Add Buttons */\n.add-button {\n background-color: transparent;\n color: #FF5722;\n border: 1px dashed #FF5722;\n border-radius: 8px;\n padding: 10px 16px;\n cursor: pointer;\n font-weight: 500;\n display: inline-block;\n font-size: 14px;\n margin-bottom: 0;\n}\n\n.add-step-button {\n background-color: transparent;\n color: #FF5722;\n border: 1px dashed #FF5722;\n border-radius: 6px;\n padding: 6px 12px;\n cursor: pointer;\n font-weight: 500;\n font-size: 13px;\n}\n\n/* Section Headers */\n.section-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 12px;\n}", - "language": "css", - "type": "file" - }, { "name": "README.mdx", - "content": "# 🍳 Shared State Recipe Creator\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **shared state** functionality - a powerful\nfeature that enables bidirectional data flow between:\n\n1. **Frontend → Agent**: UI controls update the agent's context in real-time\n2. **Agent → Frontend**: The Copilot's recipe creations instantly update the UI\n components\n\nIt's like having a cooking buddy who not only listens to what you want but also\nupdates your recipe card as you chat - no refresh needed! ✨\n\n## How to Interact\n\nMix and match any of these parameters (or none at all - it's up to you!):\n\n- **Skill Level**: Beginner to expert 👨‍🍳\n- **Cooking Time**: Quick meals or slow cooking ⏱️\n- **Special Preferences**: Dietary needs, flavor profiles, health goals 🥗\n- **Ingredients**: Items you want to include 🧅🥩🍄\n- **Instructions**: Any specific steps\n\nThen chat with your Copilot chef with prompts like:\n\n- \"I'm a beginner cook. Can you make me a quick dinner?\"\n- \"I need something spicy with chicken that takes under 30 minutes!\"\n\n## ✨ Shared State Magic in Action\n\n**What's happening technically:**\n\n- The UI and Copilot agent share the same state object (**Agent State = UI\n State**)\n- Changes from either side automatically update the other\n- Neither side needs to manually request updates from the other\n\n**What you'll see in this demo:**\n\n- Set cooking time to 20 minutes in the UI and watch the Copilot immediately\n respect your time constraint\n- Add ingredients through the UI and see them appear in your recipe\n- When the Copilot suggests new ingredients, watch them automatically appear in\n the UI ingredients list\n- Change your skill level and see how the Copilot adapts its instructions in\n real-time\n\nThis synchronized state creates a seamless experience where the agent always has\nyour current preferences, and any updates to the recipe are instantly reflected\nin both places.\n\nThis shared state pattern can be applied to any application where you want your\nUI and Copilot to work together in perfect harmony!\n", + "content": "# 🧭 Subagents\n\n## What This Demo Shows\n\nA supervisor agent stays in charge of the conversation and calls specialist\nagents as tools (`Agent.as_tool()`), possibly several in one turn, then\nsynthesizes their outputs itself — a call-and-return delegation, not a\nhandoff (control never transfers away from the supervisor). Same shape as\nCopilotKit's own LangGraph \"subagents\" showcase demo (a supervisor calling\nchild agents as tools).\n\nA sidebar tracks the team live: role chips light up while a specialist is\nworking, and a delegation log lists each call with its status and a\npreview of the input/output.\n\n## How to Interact\n\n- \"Write a short piece about the history of coffee\" (calls research,\n writer, and critic specialists in sequence, then produces a final draft)\n- \"What is 2 + 2?\" (answered directly, no team involved)\n\n## Technical Details\n\n- Each specialist invocation is an ordinary `TOOL_CALL_START/ARGS/END` +\n `TOOL_CALL_RESULT` sequence\n- The nested agents' own model turns stay internal to the SDK and are not\n streamed as separate messages — the client sees one coherent transcript\n- The sidebar is built from three `useRenderTool` renderers (one per\n specialist tool name), each reporting its status into shared page state\n via a small tracker component\n", "language": "markdown", "type": "file" }, { - "name": "shared_state.py", - "content": "\"\"\"Shared State example for Langroid.\n\nDemonstrates bidirectional state synchronization between agent and UI for recipe collaboration.\n\"\"\"\nimport json\nimport os\nimport logging\nfrom pathlib import Path\nfrom typing import Dict, Any\nfrom dotenv import load_dotenv\n\nenv_path = Path(__file__).parent.parent.parent / '.env'\nload_dotenv(dotenv_path=env_path)\n\nimport langroid as lr\nfrom langroid.agent import ChatAgent, ChatAgentConfig, ToolMessage\nfrom langroid.language_models import OpenAIChatModel, OpenAIGPTConfig\n\nfrom ag_ui_langroid import LangroidAgent, create_langroid_app\nfrom ag_ui_langroid.types import ToolBehavior, LangroidAgentConfig, ToolCallContext\n\nlogger = logging.getLogger(__name__)\n\n\nclass GenerateRecipeTool(ToolMessage):\n \"\"\"Generate or update a recipe.\"\"\"\n request: str = \"generate_recipe\"\n purpose: str = \"\"\"\n Generate or update a recipe using the provided recipe data.\n Always provide the COMPLETE recipe, not just the changes.\n Include all fields: title, skill_level, special_preferences, cooking_time, ingredients, instructions, and changes.\n \"\"\"\n recipe: Dict[str, Any]\n\n\nllm_config = OpenAIGPTConfig(\n chat_model=OpenAIChatModel.GPT4_1_MINI,\n api_key=os.getenv(\"OPENAI_API_KEY\"),\n temperature=0.0,\n)\n\n\nclass RecipeAssistantAgent(ChatAgent):\n \"\"\"ChatAgent with recipe generation capabilities and shared state support.\"\"\"\n\n def __init__(self, config: ChatAgentConfig):\n super().__init__(config)\n self.enable_message(GenerateRecipeTool)\n\n def generate_recipe(self, msg: GenerateRecipeTool) -> str:\n \"\"\"Handle generate_recipe tool execution. State snapshot is emitted via state_from_args.\"\"\"\n return json.dumps({\"status\": \"success\", \"message\": \"Recipe generated successfully\"})\n\n\ndef build_state_context(input_data, user_message: str) -> str:\n \"\"\"Inject current recipe state into prompt.\"\"\"\n state_dict = getattr(input_data, \"state\", None) or {}\n if isinstance(state_dict, dict) and \"recipe\" in state_dict:\n recipe_json = json.dumps(state_dict[\"recipe\"], indent=2)\n return (\n f\"Current recipe state:\\n{recipe_json}\\n\\n\"\n f\"User request: {user_message}\\n\\n\"\n \"Please update the recipe by calling the generate_recipe tool with the COMPLETE updated recipe.\"\n )\n return user_message\n\n\nasync def recipe_state_from_args(context: ToolCallContext):\n \"\"\"Emit recipe snapshot as soon as tool arguments are available.\"\"\"\n try:\n if hasattr(context.tool_input, \"recipe\"):\n recipe_dict = context.tool_input.recipe\n if isinstance(recipe_dict, dict):\n return {\"recipe\": recipe_dict}\n\n if context.args_str:\n args_data = json.loads(context.args_str)\n recipe_dict = args_data.get(\"recipe\")\n if isinstance(recipe_dict, dict):\n return {\"recipe\": recipe_dict}\n\n return None\n except Exception as e:\n logger.warning(f\"Error in recipe_state_from_args: {e}\", exc_info=True)\n return None\n\n\nagent_config = ChatAgentConfig(\n name=\"RecipeAssistant\",\n llm=llm_config,\n system_message=\"\"\"You are a helpful recipe assistant. When asked to improve or modify a recipe:\n\n1. Call the generate_recipe tool ONCE with the COMPLETE updated recipe\n2. Include ALL fields: title, skill_level, special_preferences, cooking_time, ingredients, instructions, and changes\n3. After calling the tool, respond to the user with a brief confirmation of what you changed (1-2 sentences)\n4. Do NOT call the tool multiple times in a row\n5. Keep existing elements that aren't being changed\n6. Do not list the ingredients and instructions in the response, use the tool to display them, unless the user asks for them.\n\nBe creative and helpful!\"\"\",\n use_tools=True,\n use_functions_api=True,\n)\n\nchat_agent = RecipeAssistantAgent(agent_config)\n\ntask = lr.Task(\n chat_agent,\n name=\"RecipeAssistant\",\n interactive=False,\n single_round=False,\n)\n\nshared_state_config = LangroidAgentConfig(\n tool_behaviors={\n \"generate_recipe\": ToolBehavior(\n state_from_args=recipe_state_from_args,\n )\n },\n state_context_builder=build_state_context,\n)\n\nagui_agent = LangroidAgent(\n agent=task,\n name=\"shared_state\",\n description=\"A recipe assistant that collaborates with you to create amazing recipes\",\n config=shared_state_config,\n)\n\napp = create_langroid_app(agui_agent, \"/\")\n", + "name": "subagents.py", + "content": "\"\"\"Subagents — multi-agent via the SDK's agents-as-tools pattern.\n\nUnlike a handoff (control *transfers* to the specialist, triage agent exits\nthe conversation), the supervisor here stays in charge and *calls*\nspecialists as tools (``Agent.as_tool()``), possibly several in one turn,\nthen synthesizes their outputs itself — a call-and-return delegation, not a\nhandoff. Same shape as the LangGraph \"subagents\" showcase demo (a supervisor\ncalling child agents as tools and getting results back), just built with the\nSDK's own ``as_tool()`` instead of a routing graph.\n\nOn the AG-UI side each specialist invocation is an ordinary\n``TOOL_CALL_START/ARGS/END`` + ``TOOL_CALL_RESULT`` sequence — the nested\nagent's own model turns stay internal to the SDK and are not streamed as\nseparate messages, so the client sees one coherent supervisor transcript.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent\nfrom fastapi import FastAPI\n\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\nresearch_agent = Agent(\n name=\"research_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You research a topic and return the key facts as a short bullet \"\n \"list. Facts only — no fluff, no conclusions.\"\n ),\n)\n\nwriter_agent = Agent(\n name=\"writer_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You turn bullet-point facts into short, engaging prose. One tight \"\n \"paragraph unless asked otherwise.\"\n ),\n)\n\ncritic_agent = Agent(\n name=\"critic_agent\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You review a draft and return concrete improvement suggestions as a \"\n \"numbered list. Max 3 suggestions, be specific.\"\n ),\n)\n\n\ndef create_subagents_agent() -> Agent:\n return Agent(\n name=\"orchestrator\",\n model=DEFAULT_MODEL,\n instructions=(\n \"You orchestrate a small content team. For writing requests: \"\n \"call research_topic for the facts, then write_prose to draft, \"\n \"then critique_draft to review, then produce the final version \"\n \"yourself incorporating the critique. For simple questions, \"\n \"answer directly without the team.\"\n ),\n tools=[\n research_agent.as_tool(\n tool_name=\"research_topic\",\n tool_description=\"Research a topic and return key facts as bullets.\",\n ),\n writer_agent.as_tool(\n tool_name=\"write_prose\",\n tool_description=\"Turn bullet-point facts into short prose.\",\n ),\n critic_agent.as_tool(\n tool_name=\"critique_draft\",\n tool_description=\"Review a draft and suggest up to 3 improvements.\",\n ),\n ],\n )\n\n\nagent = OpenAIAgentsAgent(create_subagents_agent(), name=\"subagents\")\napp = FastAPI(title=\"Subagents AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", "language": "python", "type": "file" } ], - "watsonx::agentic_chat": [ + "openai-agents-python::custom_lifecycle_events": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useRenderTool,\n useAgentContext,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n\n useAgentContext({\n description: 'Name of the user',\n value: 'Bob'\n });\n\n useFrontendTool({\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients. Only use when asked.\"),\n }) ,\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n return {\n status: \"success\",\n message: `Background changed to ${background}`,\n };\n },\n });\n\n useRenderTool({\n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return
Loading weather...
;\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field reads as\n // undefined and the card renders empty values.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n return (\n
\n Weather in {parsed.city ?? args.location}\n
Temperature: {parsed.temperature}°C
\n
Humidity: {parsed.humidity}%
\n
Wind Speed: {parsed.windSpeed ?? parsed.wind_speed} mph
\n
Conditions: {parsed.conditions}
\n
\n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Change background\",\n message: \"Change the background to something new.\",\n },\n {\n title: \"Generate sonnet\",\n message: \"Write a short sonnet about AI.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n \n
\n \n
\n \n );\n};\n\nexport default AgenticChat;\n", + "content": "\"use client\";\n\nimport React, { useEffect, useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n useAgent,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface RunUsage {\n inputTokens: number;\n outputTokens: number;\n}\n\nconst CustomLifecycleEvents: React.FC = ({\n params,\n}) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const [usage, setUsage] = useState(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n {\n title: \"Tell me a fact about AI\",\n message: \"Tell me one interesting fact about AI.\",\n },\n ],\n available: \"before-first-message\",\n consumerAgentId: \"custom_lifecycle_events\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onCustomEvent: ({ event }) => {\n if (event.name !== \"run_usage\") return;\n const value = event.value as {\n input_tokens: number;\n output_tokens: number;\n };\n setUsage({\n inputTokens: value.input_tokens,\n outputTokens: value.output_tokens,\n });\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n {usage && (\n \n ⬇️ {usage.inputTokens} input tokens · ⬆️ {usage.outputTokens} output\n tokens\n
\n )}\n
\n \n
\n \n );\n};\n\nexport default CustomLifecycleEvents;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", + "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes an optional `end_custom_event` param —\nan AG-UI `CustomEvent` instance (anything else raises `TypeError`), default\n`None` (off). When set, one `CUSTOM` event is emitted right before\n`RUN_FINISHED`. This demo uses it to report the run's real token usage:\n`run_usage`, with `input_tokens`/`output_tokens` read off the SDK's own\n`RunResultStreaming.context_wrapper.usage` after the stream finishes — plain\nconversation otherwise, same agent as `agentic_chat`.\n\nReal usage is only known once the run is done — there's no such thing as a\ntrue token count before the model has answered — so this only fires once,\nat the end, not at both `RUN_STARTED` and `RUN_FINISHED` like a\ndemonstration bracketing pair would.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about AI.\"\n\nThe latest run's real input and output token counts appear above the chat.\n\n## Technical Details\n\n- `agents_examples/custom_lifecycle_events.py` composes the translator\n directly (`translator.to_openai`/`translator.to_agui`) instead of the\n `OpenAIAgentsAgent` wrapper, because reading the run's real usage requires\n the `RunResultStreaming` object the wrapper doesn't expose to a lifecycle\n event factory\n- `end_custom_event`'s value is a zero-argument closure over that `result`,\n resolved by the translator right before it emits the event — by then the\n stream is fully drained and `context_wrapper.usage` holds the run's actual\n totals\n- The translator itself is opaque to the event's contents — it only checks\n `isinstance(..., CustomEvent)`; the `name`/`value` shape is entirely up to\n the caller\n- The page subscribes to `run_usage`, stores the latest value in local React\n state, and renders it above the chat\n", "language": "markdown", "type": "file" }, { - "name": "index.ts", - "content": "import {\n AbstractAgent,\n type RunAgentInput,\n type BaseEvent,\n type Message,\n type ToolMessage,\n type Tool,\n EventType,\n type RunStartedEvent,\n type RunFinishedEvent,\n type RunErrorEvent,\n type TextMessageStartEvent,\n type TextMessageContentEvent,\n type TextMessageEndEvent,\n type ToolCallStartEvent,\n type ToolCallArgsEvent,\n type ToolCallEndEvent,\n type ToolCallResultEvent,\n type StepStartedEvent,\n type StepFinishedEvent,\n type MessagesSnapshotEvent,\n type RawEvent,\n} from \"@ag-ui/client\";\nimport { Observable } from \"rxjs\";\n\nconst IAM_TOKEN_URL = \"https://iam.cloud.ibm.com/identity/token\";\nconst FETCH_TIMEOUT_MS = 120_000;\nconst MAX_BUFFER_SIZE = 1024 * 1024; // 1MB\n\nexport interface WatsonxAgentConfig {\n region: string;\n instanceId: string;\n agentId: string;\n apiKey?: string;\n bearerToken?: string;\n}\n\nexport class WatsonxAgent extends AbstractAgent {\n private region: string;\n private instanceId: string;\n private watsonxAgentId: string;\n private apiKey?: string;\n private cachedToken?: string;\n private tokenExpiresAt = 0;\n private tokenRefreshPromise?: Promise;\n private activeAbortController?: AbortController;\n private stepInProgress = false;\n\n constructor(config: WatsonxAgentConfig) {\n super({ agentId: config.agentId });\n if (!config.apiKey && !config.bearerToken) {\n throw new Error(\"WatsonxAgent requires either apiKey or bearerToken\");\n }\n this.region = config.region;\n this.instanceId = config.instanceId;\n this.watsonxAgentId = config.agentId;\n this.apiKey = config.apiKey;\n this.cachedToken = config.bearerToken;\n if (config.bearerToken) {\n this.tokenExpiresAt = Date.now() + 55 * 60 * 1000;\n }\n }\n\n private get baseUrl(): string {\n return `https://api.${this.region}.watson-orchestrate.cloud.ibm.com/instances/${this.instanceId}`;\n }\n\n private async getToken(): Promise {\n if (this.cachedToken && Date.now() < this.tokenExpiresAt) {\n return this.cachedToken;\n }\n\n if (!this.apiKey) {\n throw new Error(\n \"watsonx: bearer token expired and no apiKey provided for refresh\",\n );\n }\n\n if (!this.tokenRefreshPromise) {\n this.tokenRefreshPromise = this.refreshToken().finally(() => {\n this.tokenRefreshPromise = undefined;\n });\n }\n return this.tokenRefreshPromise;\n }\n\n private async refreshToken(): Promise {\n const response = await fetch(IAM_TOKEN_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: `grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey=${encodeURIComponent(this.apiKey!)}`,\n });\n\n if (!response.ok) {\n throw new Error(\n `watsonx IAM token exchange failed: HTTP ${response.status}`,\n );\n }\n\n const data = await response.json();\n if (!data.access_token || typeof data.access_token !== \"string\") {\n throw new Error(\"watsonx IAM response missing access_token\");\n }\n if (!data.expiration || typeof data.expiration !== \"number\") {\n throw new Error(\"watsonx IAM response missing expiration\");\n }\n const token: string = data.access_token;\n this.cachedToken = token;\n this.tokenExpiresAt = data.expiration * 1000 - 60_000;\n return token;\n }\n\n run(input: RunAgentInput): Observable {\n return new Observable((subscriber) => {\n const abortController = new AbortController();\n this.activeAbortController = abortController;\n this.stream(input, subscriber, abortController.signal)\n .then(() => subscriber.complete())\n .catch((err) => {\n if (this.stepInProgress) {\n subscriber.next({\n type: EventType.STEP_FINISHED,\n stepName: \"watsonx-orchestrate\",\n } as StepFinishedEvent);\n this.stepInProgress = false;\n }\n const message =\n err instanceof Error\n ? `watsonx request failed: ${err.message.slice(0, 200)}`\n : \"watsonx request failed\";\n const errorEvent: RunErrorEvent = {\n type: EventType.RUN_ERROR,\n message,\n code: \"WATSONX_ERROR\",\n };\n subscriber.next(errorEvent);\n subscriber.complete();\n })\n .finally(() => {\n this.activeAbortController = undefined;\n });\n\n return () => abortController.abort();\n });\n }\n\n private async stream(\n input: RunAgentInput,\n subscriber: import(\"rxjs\").Subscriber,\n signal: AbortSignal,\n ): Promise {\n const { threadId, runId, messages } = input;\n\n const runStarted: RunStartedEvent = {\n type: EventType.RUN_STARTED,\n threadId,\n runId,\n };\n subscriber.next(runStarted);\n\n // Emit TOOL_CALL_RESULT for any tool messages in the input,\n // mirroring langgraph's pattern of surfacing tool results the client sent.\n for (const msg of messages) {\n if (msg.role === \"tool\") {\n const toolMsg = msg as ToolMessage;\n const toolResult: ToolCallResultEvent = {\n type: EventType.TOOL_CALL_RESULT,\n messageId: toolMsg.id,\n toolCallId: toolMsg.toolCallId,\n content: toolMsg.content,\n role: \"tool\",\n };\n subscriber.next(toolResult);\n }\n }\n\n const watsonxMessages = this.mapMessages(messages);\n\n const token = await this.getToken();\n\n const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);\n const combinedSignal = AbortSignal.any([signal, timeoutSignal]);\n\n const { messages: _, stream: _s, tools: _t, ...safeProps } =\n (input.forwardedProps ?? {}) as Record;\n const requestBody: Record = {\n ...safeProps,\n messages: watsonxMessages,\n stream: true,\n };\n\n if (input.tools && input.tools.length > 0) {\n requestBody.tools = input.tools.map((t: Tool) => ({\n type: \"function\",\n function: {\n name: t.name,\n description: t.description || \"\",\n parameters: t.parameters ?? {},\n },\n }));\n }\n\n const stepName = \"watsonx-orchestrate\";\n const stepStarted: StepStartedEvent = {\n type: EventType.STEP_STARTED,\n stepName,\n };\n subscriber.next(stepStarted);\n this.stepInProgress = true;\n\n const response = await fetch(\n `${this.baseUrl}/v1/orchestrate/${this.watsonxAgentId}/chat/completions`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n \"X-IBM-THREAD-ID\": threadId,\n },\n body: JSON.stringify(requestBody),\n signal: combinedSignal,\n },\n );\n\n if (!response.ok || !response.body) {\n throw new Error(`watsonx returned HTTP ${response.status}`);\n }\n\n const reader = response.body\n .pipeThrough(new TextDecoderStream())\n .getReader();\n\n let msgId: string | null = null;\n let msgStarted = false;\n let accumulatedContent = \"\";\n const activeToolCalls = new Map<\n number,\n { id: string; name: string; ended: boolean }\n >();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += value;\n if (buffer.length > MAX_BUFFER_SIZE) {\n throw new Error(\"watsonx SSE buffer exceeded 1MB — aborting\");\n }\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n this.processSSELine(line, subscriber, activeToolCalls, {\n get msgId() {\n return msgId;\n },\n set msgId(v: string | null) {\n msgId = v;\n },\n get msgStarted() {\n return msgStarted;\n },\n set msgStarted(v: boolean) {\n msgStarted = v;\n },\n get accumulatedContent() {\n return accumulatedContent;\n },\n set accumulatedContent(v: string) {\n accumulatedContent = v;\n },\n });\n }\n }\n\n // Process remaining buffer after stream ends\n if (buffer.trim().startsWith(\"data:\")) {\n const trimmed = buffer.trim();\n const data = trimmed.startsWith(\"data: \")\n ? trimmed.slice(6).trim()\n : trimmed.slice(5).trim();\n if (data && data !== \"[DONE]\") {\n try {\n this.processSSELine(trimmed, subscriber, activeToolCalls, {\n get msgId() {\n return msgId;\n },\n set msgId(v: string | null) {\n msgId = v;\n },\n get msgStarted() {\n return msgStarted;\n },\n set msgStarted(v: boolean) {\n msgStarted = v;\n },\n get accumulatedContent() {\n return accumulatedContent;\n },\n set accumulatedContent(v: string) {\n accumulatedContent = v;\n },\n });\n } catch (e) {\n if (!(e instanceof SyntaxError)) throw e;\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n for (const [, tc] of activeToolCalls) {\n if (!tc.ended) {\n const toolEnd: ToolCallEndEvent = {\n type: EventType.TOOL_CALL_END,\n toolCallId: tc.id,\n };\n subscriber.next(toolEnd);\n }\n }\n\n if (msgStarted && msgId) {\n const msgEnd: TextMessageEndEvent = {\n type: EventType.TEXT_MESSAGE_END,\n messageId: msgId,\n };\n subscriber.next(msgEnd);\n }\n\n // Emit STEP_FINISHED now that streaming is complete.\n const stepFinished: StepFinishedEvent = {\n type: EventType.STEP_FINISHED,\n stepName,\n };\n subscriber.next(stepFinished);\n this.stepInProgress = false;\n\n // Emit MESSAGES_SNAPSHOT with the full conversation: input messages\n // plus the assistant's response (if any text was generated).\n const snapshotMessages: Message[] = [...messages];\n if (accumulatedContent && msgId) {\n snapshotMessages.push({\n id: msgId,\n role: \"assistant\",\n content: accumulatedContent,\n });\n }\n const messagesSnapshot: MessagesSnapshotEvent = {\n type: EventType.MESSAGES_SNAPSHOT,\n messages: snapshotMessages,\n };\n subscriber.next(messagesSnapshot);\n\n const runFinished: RunFinishedEvent = {\n type: EventType.RUN_FINISHED,\n threadId,\n runId,\n };\n subscriber.next(runFinished);\n }\n\n private mapMessages(messages: Message[]): Record[] {\n return messages.map((m) => {\n const base: Record = {\n role: m.role,\n content:\n typeof m.content === \"string\" ? m.content : JSON.stringify(m.content),\n };\n if (\"toolCallId\" in m && m.toolCallId) {\n base.tool_call_id = m.toolCallId;\n }\n if (\"toolCalls\" in m && m.toolCalls) {\n base.tool_calls = m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: tc.function.name,\n arguments: tc.function.arguments || \"\",\n },\n }));\n }\n return base;\n });\n }\n\n private processSSELine(\n line: string,\n subscriber: import(\"rxjs\").Subscriber,\n activeToolCalls: Map,\n state: { msgId: string | null; msgStarted: boolean; accumulatedContent: string },\n ): void {\n // Handle both \"data: \" and \"data:\" (without trailing space)\n if (!line.startsWith(\"data:\")) return;\n const data = line.startsWith(\"data: \")\n ? line.slice(6).trim()\n : line.slice(5).trim();\n if (data === \"[DONE]\") return;\n\n let chunk: Record;\n try {\n chunk = JSON.parse(data);\n } catch {\n return;\n }\n\n // Emit a RAW event for every parsed SSE chunk, giving consumers\n // access to platform-specific data for debugging.\n const rawEvent: RawEvent = {\n type: EventType.RAW,\n event: chunk,\n source: \"watsonx\",\n };\n subscriber.next(rawEvent);\n\n const choices = chunk.choices as Array> | undefined;\n const choice = choices?.[0];\n if (!choice) return;\n\n const delta = choice.delta as Record | undefined;\n if (!delta) return;\n\n if (delta.tool_calls) {\n const toolCalls = delta.tool_calls as Array>;\n for (const tc of toolCalls) {\n const idx = (tc.index as number) ?? 0;\n const fn = tc.function as Record | undefined;\n\n if (tc.id && fn?.name) {\n activeToolCalls.set(idx, {\n id: tc.id as string,\n name: fn.name,\n ended: false,\n });\n const toolStart: ToolCallStartEvent = {\n type: EventType.TOOL_CALL_START,\n toolCallId: tc.id as string,\n toolCallName: fn.name,\n };\n subscriber.next(toolStart);\n }\n\n if (fn?.arguments != null && fn.arguments !== \"\") {\n const active = activeToolCalls.get(idx);\n if (active) {\n const toolArgs: ToolCallArgsEvent = {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: active.id,\n delta: fn.arguments,\n };\n subscriber.next(toolArgs);\n }\n }\n }\n // Don't return early — let the finish_reason check below run\n // in case the same chunk also carries finish_reason: \"tool_calls\"\n }\n\n if (delta.content != null && delta.content !== \"\") {\n if (!state.msgStarted) {\n state.msgId = crypto.randomUUID();\n const msgStart: TextMessageStartEvent = {\n type: EventType.TEXT_MESSAGE_START,\n messageId: state.msgId,\n role: \"assistant\",\n };\n subscriber.next(msgStart);\n state.msgStarted = true;\n }\n state.accumulatedContent += delta.content as string;\n const msgContent: TextMessageContentEvent = {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: state.msgId!,\n delta: delta.content as string,\n };\n subscriber.next(msgContent);\n }\n\n const finishReason = (choice as Record).finish_reason;\n if (finishReason === \"stop\" || finishReason === \"tool_calls\") {\n // Close open tool calls for \"tool_calls\" finish reason\n if (finishReason === \"tool_calls\") {\n for (const [, tc] of activeToolCalls) {\n if (!tc.ended) {\n const toolEnd: ToolCallEndEvent = {\n type: EventType.TOOL_CALL_END,\n toolCallId: tc.id,\n };\n subscriber.next(toolEnd);\n tc.ended = true;\n }\n }\n activeToolCalls.clear();\n }\n // Close open text message for \"stop\" finish reason\n if (finishReason === \"stop\" && state.msgStarted && state.msgId) {\n const msgEnd: TextMessageEndEvent = {\n type: EventType.TEXT_MESSAGE_END,\n messageId: state.msgId,\n };\n subscriber.next(msgEnd);\n state.msgStarted = false;\n }\n }\n }\n\n override abortRun(): void {\n this.activeAbortController?.abort();\n this.activeAbortController = undefined;\n }\n\n clone(): WatsonxAgent {\n // Use AbstractAgent.clone() to copy base state (messages, state,\n // description, subscribers, middlewares, pendingInterrupts, etc.)\n const cloned = super.clone() as WatsonxAgent;\n // Overlay watsonx-specific fields\n cloned.region = this.region;\n cloned.instanceId = this.instanceId;\n cloned.watsonxAgentId = this.watsonxAgentId;\n cloned.apiKey = this.apiKey;\n cloned.cachedToken = this.cachedToken;\n cloned.tokenExpiresAt = this.tokenExpiresAt;\n cloned.stepInProgress = false;\n return cloned;\n }\n}\n", - "language": "ts", + "name": "custom_lifecycle_events.py", + "content": "\"\"\"Custom lifecycle events — a real usage summary right before RUN_FINISHED.\n\nPlain chat agent, same as agentic_chat — the point isn't the agent, it's the\nCUSTOM event at the end. The SDK only knows real input/output token counts\nonce the run has finished, so this demo composes the translator directly\n(rather than the OpenAIAgentsAgent wrapper) to read them off the run result\nafter the stream completes and before to_agui() emits RUN_FINISHED.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, Runner\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import CustomEvent, EventType, RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\nagent = Agent(\n name=\"assistant\",\n model=DEFAULT_MODEL,\n instructions=\"You are a helpful assistant. Be concise.\",\n)\n\napp = FastAPI(title=\"Custom lifecycle events AG-UI demo\")\ntranslator = AGUITranslator()\n\n\n@app.post(\"/\")\nasync def run_custom_lifecycle_events(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run, then report real token usage.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n result = Runner.run_streamed(\n agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n def usage_value() -> dict[str, int]:\n # Resolved after the stream finishes, so these are the run's\n # actual totals, not an estimate made up before anything ran.\n usage = result.context_wrapper.usage\n return {\n \"input_tokens\": usage.input_tokens,\n \"output_tokens\": usage.output_tokens,\n }\n\n end_custom_event = CustomEvent(\n type=EventType.CUSTOM, name=\"run_usage\", value=usage_value\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(\n result, body, end_custom_event=end_custom_event\n ):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", + "language": "python", "type": "file" } ], - "watsonx::v1_agentic_chat": [ + "openai-agents-python::dynamic_system_prompt": [ { "name": "page.tsx", - "content": "\"use client\";\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport { CopilotChat } from \"@copilotkit/react-ui\";\nimport \"@copilotkit/react-ui/styles.css\";\n\ninterface V1AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst V1AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n
\n
\n \n
\n
\n \n );\n};\n\nexport default V1AgenticChat;\n", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { CopilotChat, useAgentContext } from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface DynamicSystemPromptProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ntype Language = \"English\" | \"Arabic\" | \"German\";\n\nconst LANGUAGES: { value: Language; label: string; flag: string }[] = [\n { value: \"English\", label: \"English\", flag: \"🇬🇧\" },\n { value: \"Arabic\", label: \"Arabic\", flag: \"🇸🇦\" },\n { value: \"German\", label: \"German\", flag: \"🇩🇪\" },\n];\n\nconst DynamicSystemPrompt: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DynamicSystemPromptView = () => {\n const [language, setLanguage] = useState(\"English\");\n\n // The only thing this demo sends: one context item the agent's\n // instructions callable reads on every turn to pick the reply language.\n // No tool, no state — just the AG-UI context channel.\n useAgentContext({\n description: \"Reply language\",\n value: language,\n });\n\n return (\n
\n
\n

Reply language

\n
\n {LANGUAGES.map(({ value, label, flag }) => (\n setLanguage(value)}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-sm text-left transition-colors ${\n language === value\n ? \"border-blue-400 bg-blue-50 dark:bg-blue-950/40\"\n : \"border-black/10 dark:border-white/10\"\n }`}\n >\n {flag}\n {label}\n \n ))}\n
\n

\n Switching language updates the AG-UI context sent with the next\n message — the agent's system prompt is rebuilt every turn to\n match.\n

\n
\n\n
\n
\n \n
\n
\n
\n );\n};\n\nexport default DynamicSystemPrompt;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# 🤖 V1 Agentic Chat\n\n## What This Demo Shows\n\nThis demo verifies **CopilotKit v1 API compatibility**. It uses the original v1\ncomponents (`CopilotKit` provider and `CopilotChat`) to ensure that v1 APIs\ncontinue to work correctly against the current runtime.\n\n1. **V1 Provider**: Uses `CopilotKit` from `@copilotkit/react-core` with the\n `agent` prop for agent selection\n2. **V1 Chat UI**: Uses `CopilotChat` from `@copilotkit/react-ui` with v1\n styling\n3. **Same Backend**: Connects to the same runtime endpoint as v2, validating\n backward compatibility\n\n## How to Interact\n\nThis is a standard chat interface — type a message and the agent will respond\nconversationally, just like the v2 agentic chat demo.\n\n## ✨ V1 Compatibility\n\n**What's happening technically:**\n\n- The v1 `CopilotKit` provider connects to the same `/api/copilotkit/[integration]` endpoint\n- The v1 chat UI renders with v1 CSS classes (`.copilotKitInput`, `.copilotKitAssistantMessage`, etc.)\n- The agent selected via the `agent` prop maps to the same `agentic_chat` backend agent\n- This ensures that applications built with v1 APIs continue to function after runtime upgrades\n", + "content": "# 🌐 Dynamic System Prompt\n\n## What This Demo Shows\n\nThe frontend sends a language choice over the AG-UI **context** channel — a\nplain `{description, value}` pair, no special class involved. The agent's\n`instructions` is a callable (`Agent.instructions`, the OpenAI Agents SDK's\nown dynamic-instructions feature) that reads the context list every turn and\nbakes \"always reply in X\" into the system prompt fresh, before each model\ncall.\n\nContext needs no `AGUIContext`/state machinery — it's read-only and\nregenerated every run. The shared `OpenAIAgentsAgent` wrapper passes the\ntranslated context into `Runner.run_streamed(..., context=...)`, so this demo\nuses the same mounted FastAPI-app pattern as the other examples.\n\n## How to Interact\n\n- Pick a language in the sidebar, then ask anything — the reply comes back\n entirely in that language, regardless of what language you type in.\n- Switch languages mid-conversation and ask again — the next reply follows\n the new choice immediately, no restart needed.\n\n## Technical Details\n\n- `useAgentContext({ description: \"Reply language\", value: language })` —\n pushes the current pick onto `RunAgentInput.context` with every message\n- `dynamic_instructions(ctx, agent)` in\n `examples/agents_examples/dynamic_system_prompt.py` reads\n `ctx.context` (the raw `list[Context]` sent by the client) and returns a\n fresh instructions string\n- The demo app is mounted by `examples/server.py` at\n `/dynamic_system_prompt`; the aggregate OpenAI Agents server runs on port\n 8024\n", "language": "markdown", "type": "file" + }, + { + "name": "dynamic_system_prompt.py", + "content": "\"\"\"Dynamic system prompt — reply language driven by the AG-UI ``context`` channel.\n\nThe frontend sends a language choice in ``RunAgentInput.context`` and the SDK\nrebuilds its instructions for that request.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom agents import Agent, RunContextWrapper\nfrom fastapi import FastAPI\n\nfrom ag_ui.core import Context\nfrom ag_ui_openai_agents import OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint\nfrom .constants import DEFAULT_MODEL\n\nBASE_INSTRUCTIONS = (\n \"You are a helpful, concise assistant. Answer the user's questions directly.\"\n)\n\n# Fallback when the frontend hasn't picked a language yet.\nDEFAULT_LANGUAGE = \"English\"\n\n\ndef _read_language(ctx: RunContextWrapper[list[Context]]) -> str:\n \"\"\"Pull the reply language out of the AG-UI context list.\n\n ``ctx.context`` here IS the raw ``list[Context]`` the client sent —\n each item a ``{description, value}`` pair, nothing wrapping it. We match\n the item whose description mentions \"language\" and use its value.\n \"\"\"\n items = ctx.context or []\n for item in items:\n if \"language\" in (item.description or \"\").lower():\n return item.value or DEFAULT_LANGUAGE\n return DEFAULT_LANGUAGE\n\n\ndef dynamic_instructions(ctx: RunContextWrapper[list[Context]], agent: Agent) -> str:\n \"\"\"Native SDK dynamic-instructions hook: build the prompt fresh each turn,\n baking in whatever language the frontend currently has selected.\"\"\"\n language = _read_language(ctx)\n return (\n f\"{BASE_INSTRUCTIONS}\\n\"\n f\"Always reply in {language}, no matter what language the user writes in. \"\n f\"Every word of your response must be in {language}.\"\n )\n\n\ndef create_dynamic_system_prompt_agent() -> Agent:\n return Agent(\n name=\"multilingual_assistant\",\n model=DEFAULT_MODEL,\n instructions=dynamic_instructions,\n )\n\n\nagent = OpenAIAgentsAgent(create_dynamic_system_prompt_agent(), name=\"dynamic_system_prompt\")\napp = FastAPI(title=\"Dynamic system prompt AG-UI demo\")\nadd_openai_agents_fastapi_endpoint(app, agent, \"/\")\n", + "language": "python", + "type": "file" } ] } \ No newline at end of file diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index 78375f23bc..5a8dc9d696 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -377,21 +377,6 @@ export const menuIntegrations = [ "tool_based_generative_ui", ], }, - { - id: "openai-agents-python", - name: "OpenAI Agents SDK (Python)", - features: [ - "ag_ui_docs_copilot", - "agentic_chat", - "backend_tool_rendering", - "human_in_the_loop", - "human_in_the_loop_approval", - "tool_based_generative_ui", - "subagents", - "custom_lifecycle_events", - "dynamic_system_prompt", - ], - }, { id: "langroid", name: "Langroid", @@ -407,4 +392,19 @@ export const menuIntegrations = [ name: "IBM watsonx orchestrate", features: ["agentic_chat", "v1_agentic_chat"], }, + { + id: "openai-agents-python", + name: "OpenAI Agents (Python)", + features: [ + "ag_ui_docs_copilot", + "agentic_chat", + "backend_tool_rendering", + "human_in_the_loop", + "human_in_the_loop_approval", + "tool_based_generative_ui", + "subagents", + "custom_lifecycle_events", + "dynamic_system_prompt", + ], + }, ] as const satisfies MenuIntegrationConfig[]; diff --git a/integrations/openai-agents/typescript/src/index.test.ts b/integrations/openai-agents/typescript/src/index.test.ts new file mode 100644 index 0000000000..a9478ab7d1 --- /dev/null +++ b/integrations/openai-agents/typescript/src/index.test.ts @@ -0,0 +1,19 @@ +import { HttpAgent } from "@ag-ui/client"; +import { describe, expect, it } from "vitest"; + +import { OpenAIAgentsHttpAgent } from "./index"; + +describe("OpenAIAgentsHttpAgent", () => { + it("extends HttpAgent", () => { + expect(OpenAIAgentsHttpAgent.prototype).toBeInstanceOf(HttpAgent); + }); + + it("can be created with a URL", () => { + const agent = new OpenAIAgentsHttpAgent({ + url: "http://localhost:8024/agentic_chat/", + }); + + expect(agent).toBeInstanceOf(OpenAIAgentsHttpAgent); + expect(agent).toBeInstanceOf(HttpAgent); + }); +}); From 84b66763b0f77d804c4ddc7dc833a6f37b527395 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 14:27:19 +0200 Subject: [PATCH 88/94] docs(openai-agents): update references to OpenAI Agents SDK to OpenAI Agents (Python) --- README.md | 2 +- .../[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx | 2 +- apps/dojo/src/menu.ts | 2 +- docs/introduction.mdx | 2 +- integrations/openai-agents/python/pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8db4b044d5..1f2ab59769 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ AG-UI was born from CopilotKit's initial **partnership** with LangGraph and Crew | ---------- | ------- | ---------------- | | [Claude Agent SDK](https://github.com/ag-ui-protocol/ag-ui/tree/main/integrations/claude-agent-sdk) | ✅ Supported | 🎮 [Demos](https://dojo.ag-ui.com/claude-agent-sdk-python/feature/shared_state) | | [Langroid](https://github.com/ag-ui-protocol/ag-ui/tree/main/integrations/langroid) | ✅ Supported | 🎮 [Demos](https://dojo.ag-ui.com/langroid/feature/shared_state) | -| [OpenAI Agent SDK](https://openai.github.io/openai-agents-python/) | 🛠️ In Progress | – | +| [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) | 🛠️ In Progress | – | | [Cloudflare Agents](https://developers.cloudflare.com/agents/) | 🛠️ In Progress | – | diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx index a3dea9a764..a9de27bc48 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx @@ -4,7 +4,7 @@ This example is a small documentation assistant for the AG-UI integration with the OpenAI Agents SDK. One **Copilot** agent handles normal conversation directly, and for AG-UI or -OpenAI Agents SDK questions calls one of two tools, `read_ag_ui_protocol_docs` +OpenAI Agents SDK questions, it calls one of two tools, `read_ag_ui_protocol_docs` and `read_ag_ui_openai_agents_docs`, to read a single section of the relevant local README by heading. diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index 5a8dc9d696..abd5b78c6f 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -394,7 +394,7 @@ export const menuIntegrations = [ }, { id: "openai-agents-python", - name: "OpenAI Agents (Python)", + name: "OpenAI Agents SDK (Python)", features: [ "ag_ui_docs_copilot", "agentic_chat", diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 43b40f2784..3be4079ce5 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -267,7 +267,7 @@ AG-UI was born from CopilotKit's initial **partnership** with LangGraph and Crew ### Agent Framework - Community | Framework | Status | AG-UI Resources | | :-------- | ------ | --------------- | -| [OpenAI Agent SDK](https://openai.github.io/openai-agents-python/) | In Progress | – | +| [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) | In Progress | – | | [Cloudflare Agents](https://developers.cloudflare.com/agents/) | In Progress | – | ### Agent Interaction Protocols diff --git a/integrations/openai-agents/python/pyproject.toml b/integrations/openai-agents/python/pyproject.toml index 4aa1733311..a38a920f75 100644 --- a/integrations/openai-agents/python/pyproject.toml +++ b/integrations/openai-agents/python/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ag-ui-openai-agents" version = "0.1.0" -description = "AG-UI integration for OpenAI Agent SDK" +description = "AG-UI integration for OpenAI Agents SDK" license = "MIT" license-files = ["LICENSE"] readme = "README.md" From a8f78927c81dfccce944b9a8ba05ff27b375bf0e Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 15:17:00 +0200 Subject: [PATCH 89/94] fix(openai-agents): preserve per-message usage labels --- .../customLifecycleEventsPage.spec.ts | 13 ++-- .../(v2)/custom_lifecycle_events/README.mdx | 7 +- .../(v2)/custom_lifecycle_events/page.tsx | 77 +++++++++++++++---- apps/dojo/src/files.json | 10 +-- 4 files changed, 77 insertions(+), 30 deletions(-) diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/customLifecycleEventsPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/customLifecycleEventsPage.spec.ts index ee2e10b006..77b319e344 100644 --- a/apps/dojo/e2e/tests/openaiAgentsPythonTests/customLifecycleEventsPage.spec.ts +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/customLifecycleEventsPage.spec.ts @@ -1,15 +1,18 @@ import { expect, test } from "../../test-isolation-helper"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; -test("[OpenAI Agents Python] Custom Lifecycle Events displays real token usage", async ({ +test("[OpenAI Agents Python] Custom Lifecycle Events keeps usage with each reply", async ({ page, }) => { await page.goto("/openai-agents-python/feature/custom_lifecycle_events"); await sendAndAwaitResponse(page, "Hi"); + await sendAndAwaitResponse(page, "Tell me one interesting fact about AI."); - const usage = page.getByLabel("Run usage").last(); - await expect(usage).toBeVisible(); - await expect(usage).toContainText(/\d+ input tokens/); - await expect(usage).toContainText(/\d+ output tokens/); + const usageLabels = page.getByLabel("Run usage"); + await expect(usageLabels).toHaveCount(2); + for (const usage of await usageLabels.all()) { + await expect(usage).toContainText(/\d+ input tokens/); + await expect(usage).toContainText(/\d+ output tokens/); + } }); diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx index b740c0c0da..97238c5278 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx @@ -20,7 +20,8 @@ demonstration bracketing pair would. - "Say hi in one sentence." - "Tell me one interesting fact about AI." -The latest run's real input and output token counts appear above the chat. +Each assistant reply has a compact usage label immediately after it, showing +the real input and output token counts for that run. ## Technical Details @@ -36,5 +37,5 @@ The latest run's real input and output token counts appear above the chat. - The translator itself is opaque to the event's contents — it only checks `isinstance(..., CustomEvent)`; the `name`/`value` shape is entirely up to the caller -- The page subscribes to `run_usage`, stores the latest value in local React - state, and renders it above the chat +- The page associates each `run_usage` event with the completed assistant + message and renders the usage directly after that message diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx index 46b6e28676..dda49c6a22 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/page.tsx @@ -1,13 +1,13 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useRef, useSyncExternalStore } from "react"; import "@copilotkit/react-core/v2/styles.css"; import { CopilotChat, + CopilotKitProvider, useAgent, useConfigureSuggestions, } from "@copilotkit/react-core/v2"; -import { CopilotKit } from "@copilotkit/react-core"; interface CustomLifecycleEventsProps { params: Promise<{ integrationId: string }>; @@ -18,25 +18,66 @@ interface RunUsage { outputTokens: number; } +const usageByMessageId = new Map(); +const usageListeners = new Set<() => void>(); + +function setMessageUsage(messageId: string, usage: RunUsage) { + usageByMessageId.set(messageId, usage); + usageListeners.forEach((listener) => listener()); +} + +function subscribeUsage(listener: () => void) { + usageListeners.add(listener); + return () => usageListeners.delete(listener); +} + +function UsageLabel({ + message, + position, +}: { + message: { id: string; role: string }; + position: "before" | "after"; +}) { + const usage = useSyncExternalStore( + subscribeUsage, + () => usageByMessageId.get(message.id), + () => undefined, + ); + if (message.role !== "assistant" || position !== "after" || !usage) { + return null; + } + + return ( +
+ ⬇️ {usage.inputTokens} input tokens · ⬆️ {usage.outputTokens} output + tokens +
+ ); +} + const CustomLifecycleEvents: React.FC = ({ params, }) => { const { integrationId } = React.use(params); + const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []); return ( - - + ); }; const Chat = () => { const { agent } = useAgent({ agentId: "custom_lifecycle_events" }); - const [usage, setUsage] = useState(null); + const completedMessageId = useRef(null); useConfigureSuggestions({ suggestions: [ @@ -52,13 +93,24 @@ const Chat = () => { useEffect(() => { const subscription = agent.subscribe({ + onRunStartedEvent: () => { + completedMessageId.current = null; + }, + onMessagesSnapshotEvent: ({ event }) => { + const assistant = [...event.messages] + .reverse() + .find((message) => message.role === "assistant"); + completedMessageId.current = assistant?.id ?? null; + }, onCustomEvent: ({ event }) => { if (event.name !== "run_usage") return; + if (!completedMessageId.current) return; + const value = event.value as { input_tokens: number; output_tokens: number; }; - setUsage({ + setMessageUsage(completedMessageId.current, { inputTokens: value.input_tokens, outputTokens: value.output_tokens, }); @@ -68,16 +120,7 @@ const Chat = () => { }, [agent]); return ( -
- {usage && ( -
- ⬇️ {usage.inputTokens} input tokens · ⬆️ {usage.outputTokens} output - tokens -
- )} +
;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"read_ag_ui_openai_agents_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ heading: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n useRenderTool({\n name: \"read_ag_ui_protocol_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ heading: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents (Python) streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents (Python) streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n source,\n status,\n heading,\n result,\n}: {\n source: \"AG-UI OpenAI Agents\" | \"AG-UI Protocol\";\n status: \"inProgress\" | \"executing\" | \"complete\";\n heading?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const missed = complete && !!result && result.startsWith(\"No section matches\");\n\n return (\n
\n \n {missed ? \"!\" : complete ? \"✓\" : \"📖\"}\n \n
\n
\n \n {source} docs\n \n {!complete && (\n \n \n \n •\n \n \n •\n \n \n )}\n
\n
\n {heading\n ? `${complete ? \"Read\" : \"Reading\"} \"${heading}\"`\n : complete\n ? \"Section read\"\n : \"Opening the guide\"}\n
\n
\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", + "content": "\"use client\";\n\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport {\n CopilotChat,\n useConfigureSuggestions,\n useRenderTool,\n} from \"@copilotkit/react-core/v2\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { z } from \"zod\";\n\ninterface AGUIDocsCopilotProps {\n params: Promise<{ integrationId: string }>;\n}\n\nconst AGUIDocsCopilot: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst DocsChat = () => {\n useRenderTool({\n name: \"read_ag_ui_openai_agents_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ heading: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n useRenderTool({\n name: \"read_ag_ui_protocol_docs\",\n agentId: \"ag_ui_docs_copilot\",\n parameters: z.object({ heading: z.string().optional() }),\n render: ({ status, args, result }: any) => (\n \n ),\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Stream an existing agent\",\n message:\n \"Show me how to transfer an existing OpenAI Agents SDK streaming run to AG-UI. Give the smallest FastAPI endpoint using AGUITranslator.to_openai(), Runner.run_streamed(), AGUITranslator.to_agui(), EventEncoder, and StreamingResponse. Explain only the data flow at each boundary.\",\n },\n {\n title: \"Map SDK events to AG-UI\",\n message:\n \"Give me one concise table that maps OpenAI Agents SDK streaming events and items to AG-UI events. Include run lifecycle, text messages, tool calls and results, reasoning, state snapshots, messages snapshots, and errors. Include only mappings supported by this integration.\",\n },\n {\n title: \"Choose an integration layer\",\n message:\n \"Compare the three integration APIs: AGUITranslator, OpenAIAgentsAgent, and add_openai_agents_fastapi_endpoint. Give one concise table with what each does, when to choose it, and how much control it keeps over the SDK agent and server.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nfunction DocsLookupProgress({\n source,\n status,\n heading,\n result,\n}: {\n source: \"AG-UI OpenAI Agents\" | \"AG-UI Protocol\";\n status: \"inProgress\" | \"executing\" | \"complete\";\n heading?: string;\n result?: string;\n}) {\n const complete = status === \"complete\";\n const missed = complete && !!result && result.startsWith(\"No section matches\");\n\n return (\n
\n \n {missed ? \"!\" : complete ? \"✓\" : \"📖\"}\n \n
\n
\n \n {source} docs\n \n {!complete && (\n \n \n \n •\n \n \n •\n \n \n )}\n
\n
\n {heading\n ? `${complete ? \"Read\" : \"Reading\"} \"${heading}\"`\n : complete\n ? \"Section read\"\n : \"Opening the guide\"}\n
\n
\n
\n );\n}\n\nexport default AGUIDocsCopilot;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nOpenAI Agents (Python).\n\nOne **Copilot** agent handles normal conversation directly, and for AG-UI or\nOpenAI Agents (Python) questions call one of two tools, `read_ag_ui_protocol_docs`\nand `read_ag_ui_openai_agents_docs`, to read a single section of the relevant\nlocal README by heading.\n\n## What it demonstrates\n\n```text\nCopilot → read_ag_ui_protocol_docs / read_ag_ui_openai_agents_docs (local READMEs, one section at a time)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- Table-of-contents instructions plus an on-demand section-read tool, instead\n of loading a whole README into the prompt\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded by section\n\nThe integration README is small enough for this focused demo, so its heading\nlist sits in the Copilot's instructions and each tool call reads back one\nmatching section. This keeps the example deterministic, cheap, and fast to\nstream — an earlier version routed each question through a second\nsub-agent, which cost extra model round trips before the first token showed\nup. A production assistant with many or large documents should replace this\nwith its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask for a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", + "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nOne **Copilot** agent handles normal conversation directly, and for AG-UI or\nOpenAI Agents SDK questions, it calls one of two tools, `read_ag_ui_protocol_docs`\nand `read_ag_ui_openai_agents_docs`, to read a single section of the relevant\nlocal README by heading.\n\n## What it demonstrates\n\n```text\nCopilot → read_ag_ui_protocol_docs / read_ag_ui_openai_agents_docs (local READMEs, one section at a time)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- Table-of-contents instructions plus an on-demand section-read tool, instead\n of loading a whole README into the prompt\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded by section\n\nThe integration README is small enough for this focused demo, so its heading\nlist sits in the Copilot's instructions and each tool call reads back one\nmatching section. This keeps the example deterministic, cheap, and fast to\nstream — an earlier version routed each question through a second\nsub-agent, which cost extra model round trips before the first token showed\nup. A production assistant with many or large documents should replace this\nwith its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask for a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", "language": "markdown", "type": "file" }, { "name": "ag_ui_docs_copilot.py", - "content": "\"\"\"AG-UI documentation assistant that reads local README sections on demand.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom importlib.metadata import metadata\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n_HEADING_RE = re.compile(r\"^#{1,6}\\s+(.+)$\")\n\n\ndef _load_distribution_readme(distribution: str) -> str:\n \"\"\"Load the installed package README from its distribution metadata.\"\"\"\n document = metadata(distribution).get_payload()\n if not isinstance(document, str) or not document.strip():\n raise RuntimeError(f\"{distribution} has no README in its package metadata\")\n return document\n\n\nclass MarkdownSections:\n \"\"\"A Markdown document split by heading, read one section at a time.\n\n The Copilot sees only the heading list in its instructions and fetches\n the sections it needs on demand, so the full document never sits in a\n prompt.\n \"\"\"\n\n def __init__(self, document: str) -> None:\n sections: dict[str, list[str]] = {}\n heading = \"Overview\"\n for line in document.splitlines():\n match = _HEADING_RE.match(line)\n if match:\n heading = match.group(1).strip()\n sections.setdefault(heading, []).append(line)\n self._sections = {\n name: content\n for name, lines in sections.items()\n if (content := \"\\n\".join(lines).strip())\n }\n\n @property\n def headings(self) -> str:\n \"\"\"The table of contents: one heading per line.\"\"\"\n return \"\\n\".join(f\"- {name}\" for name in self._sections)\n\n def read(self, heading: str) -> str:\n \"\"\"Return one section by heading (case-insensitive, substring ok).\"\"\"\n wanted = heading.strip().lstrip(\"#\").strip().lower()\n for name, content in self._sections.items():\n if name.lower() == wanted:\n return content\n for name, content in self._sections.items():\n if wanted in name.lower():\n return content\n return f\"No section matches {heading!r}. Available headings:\\n{self.headings}\"\n\n\n# ag-ui-protocol docs: the core Python SDK README.\nAG_UI_PROTOCOL_DOCS = _load_distribution_readme(\"ag-ui-protocol\")\n_AG_UI_PROTOCOL_SECTIONS = MarkdownSections(AG_UI_PROTOCOL_DOCS)\n\n\n@function_tool\ndef read_ag_ui_protocol_docs(heading: str) -> str:\n \"\"\"Read one AG-UI Protocol Python documentation section by its heading.\n\n Args:\n heading: A heading from the \"AG-UI Protocol docs\" table of contents\n in your instructions.\n \"\"\"\n return _AG_UI_PROTOCOL_SECTIONS.read(heading)\n\n\n# ag-ui-openai-agents docs: this integration's README.\nAG_UI_OPENAI_AGENTS_DOCS = _load_distribution_readme(\"ag-ui-openai-agents\")\n_AG_UI_OPENAI_AGENTS_SECTIONS = MarkdownSections(AG_UI_OPENAI_AGENTS_DOCS)\n\n\n@function_tool\ndef read_ag_ui_openai_agents_docs(heading: str) -> str:\n \"\"\"Read one AG-UI OpenAI Agents integration documentation section.\n\n Args:\n heading: A heading from the \"AG-UI OpenAI Agents docs\" table of\n contents in your instructions.\n \"\"\"\n return _AG_UI_OPENAI_AGENTS_SECTIONS.read(heading)\n\n\ncopilot_instructions = f\"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question about AG-UI, OpenAI Agents (Python), translator APIs, FastAPI\nendpoints, streaming, tools, client tools, state, lifecycle events, errors,\ntests, or Python implementation, pick the most relevant heading from the\nmatching table of contents below and call the matching tool before answering.\nUse read_ag_ui_openai_agents_docs for OpenAI Agents (Python) integration questions\nand read_ag_ui_protocol_docs for ag_ui.core protocol types and EventEncoder\nquestions. Treat the returned section as your only source of truth. Read\nanother section if the first one is not enough.\n\nUse only the part of the section that answers the user's question. Do not\ninvent behavior or claim details the documentation did not provide. For code\nrequests, provide only the smallest complete, production-readable Python\nsnippet needed, followed by a short explanation. Use documented APIs only.\n\nAG-UI Protocol docs table of contents:\n{_AG_UI_PROTOCOL_SECTIONS.headings}\n\nAG-UI OpenAI Agents docs table of contents:\n{_AG_UI_OPENAI_AGENTS_SECTIONS.headings}\n\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[read_ag_ui_protocol_docs, read_ag_ui_openai_agents_docs],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", + "content": "\"\"\"AG-UI documentation assistant that reads local README sections on demand.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom importlib.metadata import metadata\n\nfrom agents import Agent, Runner, function_tool\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse\n\nfrom ag_ui.core import RunAgentInput\nfrom ag_ui.encoder import EventEncoder\nfrom ag_ui_openai_agents import AGUITranslator\nfrom .constants import DEFAULT_MODEL\n\n_HEADING_RE = re.compile(r\"^#{1,6}\\s+(.+)$\")\n\n\ndef _load_distribution_readme(distribution: str) -> str:\n \"\"\"Load the installed package README from its distribution metadata.\"\"\"\n document = metadata(distribution).get_payload()\n if not isinstance(document, str) or not document.strip():\n raise RuntimeError(f\"{distribution} has no README in its package metadata\")\n return document\n\n\nclass MarkdownSections:\n \"\"\"A Markdown document split by heading, read one section at a time.\n\n The Copilot sees only the heading list in its instructions and fetches\n the sections it needs on demand, so the full document never sits in a\n prompt.\n \"\"\"\n\n def __init__(self, document: str) -> None:\n sections: dict[str, list[str]] = {}\n heading = \"Overview\"\n for line in document.splitlines():\n match = _HEADING_RE.match(line)\n if match:\n heading = match.group(1).strip()\n sections.setdefault(heading, []).append(line)\n self._sections = {\n name: content\n for name, lines in sections.items()\n if (content := \"\\n\".join(lines).strip())\n }\n\n @property\n def headings(self) -> str:\n \"\"\"The table of contents: one heading per line.\"\"\"\n return \"\\n\".join(f\"- {name}\" for name in self._sections)\n\n def read(self, heading: str) -> str:\n \"\"\"Return one section by heading (case-insensitive, substring ok).\"\"\"\n wanted = heading.strip().lstrip(\"#\").strip().lower()\n for name, content in self._sections.items():\n if name.lower() == wanted:\n return content\n for name, content in self._sections.items():\n if wanted in name.lower():\n return content\n return f\"No section matches {heading!r}. Available headings:\\n{self.headings}\"\n\n\n# ag-ui-protocol docs: the core Python SDK README.\nAG_UI_PROTOCOL_DOCS = _load_distribution_readme(\"ag-ui-protocol\")\n_AG_UI_PROTOCOL_SECTIONS = MarkdownSections(AG_UI_PROTOCOL_DOCS)\n\n\n@function_tool\ndef read_ag_ui_protocol_docs(heading: str) -> str:\n \"\"\"Read one AG-UI Protocol Python documentation section by its heading.\n\n Args:\n heading: A heading from the \"AG-UI Protocol docs\" table of contents\n in your instructions.\n \"\"\"\n return _AG_UI_PROTOCOL_SECTIONS.read(heading)\n\n\n# ag-ui-openai-agents docs: this integration's README.\nAG_UI_OPENAI_AGENTS_DOCS = _load_distribution_readme(\"ag-ui-openai-agents\")\n_AG_UI_OPENAI_AGENTS_SECTIONS = MarkdownSections(AG_UI_OPENAI_AGENTS_DOCS)\n\n\n@function_tool\ndef read_ag_ui_openai_agents_docs(heading: str) -> str:\n \"\"\"Read one AG-UI OpenAI Agents integration documentation section.\n\n Args:\n heading: A heading from the \"AG-UI OpenAI Agents docs\" table of\n contents in your instructions.\n \"\"\"\n return _AG_UI_OPENAI_AGENTS_SECTIONS.read(heading)\n\n\ncopilot_instructions = f\"\"\"You are the developer-facing Copilot for an AG-UI\napplication. Answer only what the user asks. Be clear, practical, and concise;\ndo not add a tutorial, unrelated details, alternatives, or follow-up work\nunless requested. Handle ordinary conversation directly.\n\nFor any question about AG-UI, the OpenAI Agents SDK, translator APIs, FastAPI\nendpoints, streaming, tools, client tools, state, lifecycle events, errors,\ntests, or Python implementation, pick the most relevant heading from the\nmatching table of contents below and call the matching tool before answering.\nUse read_ag_ui_openai_agents_docs for OpenAI Agents SDK integration questions\nand read_ag_ui_protocol_docs for ag_ui.core protocol types and EventEncoder\nquestions. Treat the returned section as your only source of truth. Read\nanother section if the first one is not enough.\n\nUse only the part of the section that answers the user's question. Do not\ninvent behavior or claim details the documentation did not provide. For code\nrequests, provide only the smallest complete, production-readable Python\nsnippet needed, followed by a short explanation. Use documented APIs only.\n\nAG-UI Protocol docs table of contents:\n{_AG_UI_PROTOCOL_SECTIONS.headings}\n\nAG-UI OpenAI Agents docs table of contents:\n{_AG_UI_OPENAI_AGENTS_SECTIONS.headings}\n\"\"\"\n\ncopilot_agent = Agent(\n name=\"AG-UI Docs Copilot\",\n model=DEFAULT_MODEL,\n instructions=copilot_instructions,\n tools=[read_ag_ui_protocol_docs, read_ag_ui_openai_agents_docs],\n)\n\n# AGUI Translator Integration\napp = FastAPI(title=\"AG-UI Docs Copilot\")\ntranslator = AGUITranslator()\n\n\n@app.post(\"/\")\nasync def run_ag_ui_docs_copilot(\n body: RunAgentInput, request: Request\n) -> StreamingResponse:\n \"\"\"Translate one AG-UI request into an SDK run and stream it back.\"\"\"\n encoder = EventEncoder(accept=request.headers.get(\"accept\"))\n\n async def stream():\n # AGUI input -> OpenAI SDK\n translated_input = translator.to_openai(body)\n\n # normal OpenAI SDK streaming run\n result = Runner.run_streamed(\n copilot_agent,\n input=translated_input.messages,\n context=translated_input.context,\n )\n\n # OpenAI SDK -> AGUI events\n async for event in translator.to_agui(result, body):\n yield encoder.encode(event)\n\n return StreamingResponse(stream(), media_type=encoder.get_content_type())\n", "language": "python", "type": "file" } @@ -5100,13 +5100,13 @@ "openai-agents-python::custom_lifecycle_events": [ { "name": "page.tsx", - "content": "\"use client\";\n\nimport React, { useEffect, useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n useAgent,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface RunUsage {\n inputTokens: number;\n outputTokens: number;\n}\n\nconst CustomLifecycleEvents: React.FC = ({\n params,\n}) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const [usage, setUsage] = useState(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n {\n title: \"Tell me a fact about AI\",\n message: \"Tell me one interesting fact about AI.\",\n },\n ],\n available: \"before-first-message\",\n consumerAgentId: \"custom_lifecycle_events\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onCustomEvent: ({ event }) => {\n if (event.name !== \"run_usage\") return;\n const value = event.value as {\n input_tokens: number;\n output_tokens: number;\n };\n setUsage({\n inputTokens: value.input_tokens,\n outputTokens: value.output_tokens,\n });\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n {usage && (\n \n ⬇️ {usage.inputTokens} input tokens · ⬆️ {usage.outputTokens} output\n tokens\n
\n )}\n
\n \n
\n
\n );\n};\n\nexport default CustomLifecycleEvents;\n", + "content": "\"use client\";\n\nimport React, { useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n CopilotChat,\n CopilotKitProvider,\n useAgent,\n useConfigureSuggestions,\n} from \"@copilotkit/react-core/v2\";\n\ninterface CustomLifecycleEventsProps {\n params: Promise<{ integrationId: string }>;\n}\n\ninterface RunUsage {\n inputTokens: number;\n outputTokens: number;\n}\n\nconst usageByMessageId = new Map();\nconst usageListeners = new Set<() => void>();\n\nfunction setMessageUsage(messageId: string, usage: RunUsage) {\n usageByMessageId.set(messageId, usage);\n usageListeners.forEach((listener) => listener());\n}\n\nfunction subscribeUsage(listener: () => void) {\n usageListeners.add(listener);\n return () => usageListeners.delete(listener);\n}\n\nfunction UsageLabel({\n message,\n position,\n}: {\n message: { id: string; role: string };\n position: \"before\" | \"after\";\n}) {\n const usage = useSyncExternalStore(\n subscribeUsage,\n () => usageByMessageId.get(message.id),\n () => undefined,\n );\n if (message.role !== \"assistant\" || position !== \"after\" || !usage) {\n return null;\n }\n\n return (\n \n ⬇️ {usage.inputTokens} input tokens · ⬆️ {usage.outputTokens} output\n tokens\n
\n );\n}\n\nconst CustomLifecycleEvents: React.FC = ({\n params,\n}) => {\n const { integrationId } = React.use(params);\n const renderCustomMessages = useMemo(() => [{ render: UsageLabel }], []);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const { agent } = useAgent({ agentId: \"custom_lifecycle_events\" });\n const completedMessageId = useRef(null);\n\n useConfigureSuggestions({\n suggestions: [\n { title: \"Say hi\", message: \"Say hi in one sentence.\" },\n {\n title: \"Tell me a fact about AI\",\n message: \"Tell me one interesting fact about AI.\",\n },\n ],\n available: \"before-first-message\",\n consumerAgentId: \"custom_lifecycle_events\",\n });\n\n useEffect(() => {\n const subscription = agent.subscribe({\n onRunStartedEvent: () => {\n completedMessageId.current = null;\n },\n onMessagesSnapshotEvent: ({ event }) => {\n const assistant = [...event.messages]\n .reverse()\n .find((message) => message.role === \"assistant\");\n completedMessageId.current = assistant?.id ?? null;\n },\n onCustomEvent: ({ event }) => {\n if (event.name !== \"run_usage\") return;\n if (!completedMessageId.current) return;\n\n const value = event.value as {\n input_tokens: number;\n output_tokens: number;\n };\n setMessageUsage(completedMessageId.current, {\n inputTokens: value.input_tokens,\n outputTokens: value.output_tokens,\n });\n },\n });\n return () => subscription.unsubscribe();\n }, [agent]);\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nexport default CustomLifecycleEvents;\n", "language": "typescript", "type": "file" }, { "name": "README.mdx", - "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes an optional `end_custom_event` param —\nan AG-UI `CustomEvent` instance (anything else raises `TypeError`), default\n`None` (off). When set, one `CUSTOM` event is emitted right before\n`RUN_FINISHED`. This demo uses it to report the run's real token usage:\n`run_usage`, with `input_tokens`/`output_tokens` read off the SDK's own\n`RunResultStreaming.context_wrapper.usage` after the stream finishes — plain\nconversation otherwise, same agent as `agentic_chat`.\n\nReal usage is only known once the run is done — there's no such thing as a\ntrue token count before the model has answered — so this only fires once,\nat the end, not at both `RUN_STARTED` and `RUN_FINISHED` like a\ndemonstration bracketing pair would.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about AI.\"\n\nThe latest run's real input and output token counts appear above the chat.\n\n## Technical Details\n\n- `agents_examples/custom_lifecycle_events.py` composes the translator\n directly (`translator.to_openai`/`translator.to_agui`) instead of the\n `OpenAIAgentsAgent` wrapper, because reading the run's real usage requires\n the `RunResultStreaming` object the wrapper doesn't expose to a lifecycle\n event factory\n- `end_custom_event`'s value is a zero-argument closure over that `result`,\n resolved by the translator right before it emits the event — by then the\n stream is fully drained and `context_wrapper.usage` holds the run's actual\n totals\n- The translator itself is opaque to the event's contents — it only checks\n `isinstance(..., CustomEvent)`; the `name`/`value` shape is entirely up to\n the caller\n- The page subscribes to `run_usage`, stores the latest value in local React\n state, and renders it above the chat\n", + "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes an optional `end_custom_event` param —\nan AG-UI `CustomEvent` instance (anything else raises `TypeError`), default\n`None` (off). When set, one `CUSTOM` event is emitted right before\n`RUN_FINISHED`. This demo uses it to report the run's real token usage:\n`run_usage`, with `input_tokens`/`output_tokens` read off the SDK's own\n`RunResultStreaming.context_wrapper.usage` after the stream finishes — plain\nconversation otherwise, same agent as `agentic_chat`.\n\nReal usage is only known once the run is done — there's no such thing as a\ntrue token count before the model has answered — so this only fires once,\nat the end, not at both `RUN_STARTED` and `RUN_FINISHED` like a\ndemonstration bracketing pair would.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about AI.\"\n\nEach assistant reply has a compact usage label immediately after it, showing\nthe real input and output token counts for that run.\n\n## Technical Details\n\n- `agents_examples/custom_lifecycle_events.py` composes the translator\n directly (`translator.to_openai`/`translator.to_agui`) instead of the\n `OpenAIAgentsAgent` wrapper, because reading the run's real usage requires\n the `RunResultStreaming` object the wrapper doesn't expose to a lifecycle\n event factory\n- `end_custom_event`'s value is a zero-argument closure over that `result`,\n resolved by the translator right before it emits the event — by then the\n stream is fully drained and `context_wrapper.usage` holds the run's actual\n totals\n- The translator itself is opaque to the event's contents — it only checks\n `isinstance(..., CustomEvent)`; the `name`/`value` shape is entirely up to\n the caller\n- The page associates each `run_usage` event with the completed assistant\n message and renders the usage directly after that message\n", "language": "markdown", "type": "file" }, From 54d93c1a99fbfcc9925bf143c4c5f14889bb8a1b Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 15:26:31 +0200 Subject: [PATCH 90/94] ci(openai-agents): add CI configuration for OpenAI Agents Python integration --- .github/workflows/unit-python-sdk.yml | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/.github/workflows/unit-python-sdk.yml b/.github/workflows/unit-python-sdk.yml index 094facd6d1..29dba7bff0 100644 --- a/.github/workflows/unit-python-sdk.yml +++ b/.github/workflows/unit-python-sdk.yml @@ -10,6 +10,7 @@ on: - "integrations/adk-middleware/python/**" - "integrations/aws-strands/python/**" - "integrations/langroid/python/**" + - "integrations/openai-agents/python/**" - ".github/workflows/unit-python-sdk.yml" pull_request: branches: [main] @@ -20,6 +21,7 @@ on: - "integrations/adk-middleware/python/**" - "integrations/aws-strands/python/**" - "integrations/langroid/python/**" + - "integrations/openai-agents/python/**" - ".github/workflows/unit-python-sdk.yml" # Note: crew-ai uses Poetry (no uv.lock) — skipped for now. @@ -335,3 +337,44 @@ jobs: - name: Run tests working-directory: integrations/langroid/python run: uv run python -m unittest discover tests -v + + openai-agents-python: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Detect fork PR + id: fork-check + run: | + if [[ "${{ github.event_name }}" == "pull_request" && \ + "${GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME}" != "${{ github.repository }}" ]]; then + echo "prefix=fork-" >> "$GITHUB_OUTPUT" + else + echo "prefix=" >> "$GITHUB_OUTPUT" + fi + env: + GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }} + + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + version: ">=0.8.0" + + - name: Load cached venv + id: cached-uv-dependencies + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: integrations/openai-agents/python/.venv + key: ${{ steps.fork-check.outputs.prefix }}venv-${{ runner.os }}-openai-agents-python-${{ hashFiles('integrations/openai-agents/python/uv.lock') }} + + - name: Install dependencies + working-directory: integrations/openai-agents/python + run: uv sync + + - name: Run tests + working-directory: integrations/openai-agents/python + run: uv run pytest From bc05871a3f3389c5da3511281cc39fae983b898c Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 16:25:15 +0200 Subject: [PATCH 91/94] docs(openai-agents): polish ag_ui_openai_agents README.md --- integrations/openai-agents/python/README.md | 1315 ++++++++----------- 1 file changed, 562 insertions(+), 753 deletions(-) diff --git a/integrations/openai-agents/python/README.md b/integrations/openai-agents/python/README.md index d9cb0cfac6..9793662730 100644 --- a/integrations/openai-agents/python/README.md +++ b/integrations/openai-agents/python/README.md @@ -1,976 +1,785 @@ # AG-UI × OpenAI Agents SDK -Integrates the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) -with the [AG-UI Protocol](https://github.com/ag-ui-protocol/ag-ui). Build your -agent with the OpenAI SDK as usual, then translate its execution into AG-UI -events any AG-UI client (e.g. CopilotKit) can render live. +Connect an [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) +agent to any [AG-UI](https://github.com/ag-ui-protocol/ag-ui) client. The package +translates AG-UI requests into OpenAI Agents SDK input and translates the OpenAI Agents SDK's +stream back into ordered AG-UI events. -The integration is a **translator**. You keep using the OpenAI Agents SDK -normally; this package maps data only at the AG-UI boundary: +You keep your existing OpenAI agent, tools, handoffs, guardrails, model settings, +and server architecture. This integration owns only the protocol boundary. -``` -AG-UI RunAgentInput ──to_openai()──▶ SDK input items + tools -SDK streamed result ──to_agui()─▶ AG-UI BaseEvents +```text +AG-UI RunAgentInput + │ + ▼ +AGUITranslator.to_openai() + │ OpenAI Agents SDK messages, client-tool proxies, request metadata + ▼ +Runner.run_streamed(...) + │ OpenAI Agents SDK stream + ▼ +AGUITranslator.to_agui() + │ + ▼ +AG-UI lifecycle, text, tool, reasoning, step, state, and snapshot events ``` -The flow is: +## Requirements -```python -translator = AGUITranslator() +- Python 3.10 or newer +- `ag-ui-protocol >= 0.1.19` +- `openai-agents >= 0.8.4` +- An `OPENAI_API_KEY` for live OpenAI runs -translated_input = translator.to_openai(run_input) -result = Runner.run_streamed(agent, input=translated_input.messages) +## Installation -async for event in translator.to_agui(result, run_input): - ... -``` +With [uv](https://docs.astral.sh/uv/): -`to_agui(result, run_input)` also accepts `result.stream_events()` if your code already -has the SDK event iterator. Ordinary runs are wrapped with `RUN_STARTED` -(first) and `RUN_FINISHED`/`RUN_ERROR` (last); `thread_id` and `run_id` come -from `run_input`. The event just before `RUN_FINISHED` -is a `MESSAGES_SNAPSHOT` by default — see -[Messages Snapshot](#messages-snapshot). +```bash +uv add ag-ui-openai-agents +``` -## Install +Or with pip: ```bash pip install ag-ui-openai-agents ``` -For local development this package uses [uv](https://docs.astral.sh/uv/): +## Configuration + +Set the OpenAI API key in the environment where your server runs: ```bash -uv sync +export OPENAI_API_KEY="sk-..." ``` -## Quick Start: Compose It Yourself (recommended) +`AGUITranslator` does not read environment variables or change OpenAI Agents +SDK settings. With the recommended translator integration, your application +owns the OpenAI Agents SDK runner and its tracing configuration. + +If you use the `OpenAIAgentsAgent` wrapper or +`add_openai_agents_fastapi_endpoint`, you can optionally set +`OPENAI_AGENTS_DISABLE_TRACING=true` to disable OpenAI Agents SDK tracing. It +controls tracing only; it does not override the agent, model, or `RunConfig`. + +| OpenAI Agents SDK variable | Required | Purpose | +|---|---:|---| +| `OPENAI_API_KEY` | For the default OpenAI provider | Authenticates OpenAI requests. A custom provider may use different credentials. | +| `OPENAI_AGENTS_DISABLE_TRACING` | No | Optional when using the wrapper; set to `true` to disable OpenAI Agents SDK tracing. | + +Model selection, retries, and other run behavior remain OpenAI Agents SDK +settings. Pass a model to `Agent(...)` and, when needed, pass a `RunConfig` to +`OpenAIAgentsAgent` or `Runner.run_streamed(...)`. + +The bundled example server additionally reads: -**This is the recommended way to use this integration.** `AGUITranslator` is -just an events translator — it converts at the AG-UI boundary and nothing -else. You keep full control of the agent (any `Agent` config, model, -handoffs, guardrails) and full control of the backend server (FastAPI or -anything else, your own routes, your own SSE/WebSocket framing, your own -session/auth logic). Nothing about your `Runner.run_streamed` call or your -server is owned by this package — accept `RunAgentInput`, run your OpenAI -agent normally, stream AG-UI events back: +| Variable | Required | Purpose | +|---|---:|---| +| `OPENAI_DEFAULT_MODEL` | No | Overrides the model used by all examples. | +| `HOST` | No | Example server bind address; defaults to `0.0.0.0`. | +| `PORT` | No | Example server port; defaults to `8024`. | + +Keep secrets in your deployment secret manager or an uncommitted `.env` file. + +## Choose an integration level + +| Need | Use | +|---|---| +| Existing agent and custom server logic | `AGUITranslator` (recommended) | +| Fixed agent with less orchestration code | `OpenAIAgentsAgent` | +| Ready-made FastAPI SSE route | `OpenAIAgentsAgent` + `add_openai_agents_fastapi_endpoint` | +| Change one message or event mapping | Custom inbound/outbound engine subclass | + +Start with `AGUITranslator`. It leaves the OpenAI run and HTTP transport under +your control. Use the wrapper only when its standard behavior fits your server. + +## Quick start: use the translator (recommended) + +This is the same small architecture used by the `ag_ui_docs_copilot` example: +the agent owns its instructions and tools, while the endpoint only translates +the request, runs the OpenAI Agents SDK, and encodes the AG-UI stream. It accepts an AG-UI +request, runs a normal streaming OpenAI agent, and returns Server-Sent Events +(SSE). ```python from agents import Agent, Runner from ag_ui.core import RunAgentInput from ag_ui.encoder import EventEncoder -from fastapi import FastAPI +from ag_ui_openai_agents import AGUITranslator +from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse -from ag_ui_openai_agents import AGUITranslator +app = FastAPI(title="AG-UI Docs Copilot") -agent = Agent(name="assistant", instructions="Be concise.") -translator = AGUITranslator() # stateless — one instance serves every request -encoder = EventEncoder() # AG-UI's own SSE encoder -app = FastAPI() +copilot_agent = Agent( + name="AG-UI Docs Copilot", + instructions="Answer AG-UI developer questions clearly and concisely.", +) +translator = AGUITranslator() # Reusable across requests. @app.post("/") -async def run(body: RunAgentInput) -> StreamingResponse: - """Translate one AG-UI request into an SDK run and stream it back.""" +async def run_ag_ui_docs_copilot( + body: RunAgentInput, request: Request +) -> StreamingResponse: + encoder = EventEncoder(accept=request.headers.get("accept")) - async def stream(): - # AGUI input -> OpenAI SDK - translated_input = translator.to_openai(body) + async def stream(): + translated = translator.to_openai(body) - # merge client-declared tools onto this request's agent - run_agent = agent - if translated_input.tools: - run_agent = agent.clone(tools=[*agent.tools, *translated_input.tools]) + result = Runner.run_streamed( + copilot_agent, + input=translated.messages, + context=translated.context, + ) - # normal OpenAI SDK streaming run - result = Runner.run_streamed(run_agent, input=translated_input.messages) + async for event in translator.to_agui(result, body): + yield encoder.encode(event) - # OpenAI SDK -> AGUI events - async for event in translator.to_agui(result, body): - yield encoder.encode(event) - - return StreamingResponse(stream(), media_type=encoder.get_content_type()) + return StreamingResponse(stream(), media_type=encoder.get_content_type()) ``` -Test it: +The complete implementation adds the two documentation lookup tools to +`copilot_agent`; each tool reads one README section on demand. The full agent +and its endpoint are in +[`examples/agents_examples/ag_ui_docs_copilot.py`](examples/agents_examples/ag_ui_docs_copilot.py). +That file is intentionally linear: `RunAgentInput → to_openai → +Runner.run_streamed → to_agui → EventEncoder`. + +Run the app, then send an AG-UI request: ```bash -curl -N -X POST http://localhost:8000 \ +curl -N -X POST http://localhost:8000/ \ -H 'Content-Type: application/json' \ -d '{ - "thread_id": "t1", "run_id": "r1", - "messages": [{"id":"m1","role":"user","content":"Say hi in one sentence."}], - "tools": [], "state": {}, "context": [], "forwarded_props": null + "thread_id": "thread-1", + "run_id": "run-1", + "messages": [ + {"id": "user-1", "role": "user", "content": "Say hello in one sentence."} + ], + "tools": [], + "state": {}, + "context": [], + "forwarded_props": null }' ``` -Expected: `RUN_STARTED -> TEXT_MESSAGE_START -> TEXT_MESSAGE_CONTENT (xN) -> -TEXT_MESSAGE_END -> MESSAGES_SNAPSHOT -> RUN_FINISHED`. Add state sources to -emit the optional `STATE_SNAPSHOT` events. +A text-only run normally produces: -`initial_state` and `final_state` opt into the two `STATE_SNAPSHOT` slots. -Pass a static value, zero-argument function, or zero-argument async function; -`None` (the default) skips that snapshot. The initial source is resolved right -after `RUN_STARTED`; the final source is resolved after successful streaming, -just before `MESSAGES_SNAPSHOT`. This lets hooks and tools update -application-owned state: - -```python -state = dict(run_input.state or {}) -initial_snapshot = dict(state) -translated_input = translator.to_openai(run_input) - -result = Runner.run_streamed(agent, input=translated_input.messages, context=state) -async for event in translator.to_agui( - result, - run_input, - initial_state=initial_snapshot, - final_state=lambda: dict(state), -): - ... +```text +RUN_STARTED +TEXT_MESSAGE_START +TEXT_MESSAGE_CONTENT ... +TEXT_MESSAGE_END +MESSAGES_SNAPSHOT +RUN_FINISHED ``` -A full multi-demo server (chat, backend tools, human-in-the-loop, handoffs, -orchestrator) lives in [`examples/`](examples/). +## Quick start: use the FastAPI shortcut -## Quick Start: Serve an Agent (opinionated shortcut) - -Want the boilerplate above wired up for you instead? Wrap a plain SDK `Agent` -with `OpenAIAgentsAgent`, then hand it to `add_openai_agents_fastapi_endpoint` -— SSE, content negotiation, a `/health` check, lifecycle events, and the -state/messages snapshots are all wired for you. Trades away the control the -translator gives you (agent config is fixed at construction, server is -FastAPI) for less code. +`OpenAIAgentsAgent` performs the same `to_openai → run_streamed → to_agui` +flow. The endpoint helper adds an AG-UI POST route, SSE encoding, content +negotiation, and a health route. ```python from agents import Agent -from fastapi import FastAPI - from ag_ui_openai_agents import ( OpenAIAgentsAgent, add_openai_agents_fastapi_endpoint, ) - -agent = OpenAIAgentsAgent(Agent(name="assistant", instructions="Be concise.")) +from fastapi import FastAPI app = FastAPI() -add_openai_agents_fastapi_endpoint(app, agent, "/") -``` - -`OpenAIAgentsAgent`: -- holds no per-request state — one instance serves every request; the SDK - `Agent` is a config template, and client-declared tools are merged onto a - per-request `clone()`, so concurrent requests never see each other's tools; -- keeps the server tool when a client-declared tool uses the same name; the - conflicting client tool is ignored with a warning; -- takes `run_config=...` to set run-wide model settings; -- exposes `run_streamed(RunAgentInput) -> AsyncIterator[BaseEvent]` if you want - to serve it on a transport other than FastAPI. - -```python agent = OpenAIAgentsAgent( - Agent(name="assistant", instructions="Be concise.", model="gpt-5.4-mini"), + Agent(name="assistant", instructions="Be helpful and concise."), ) + +add_openai_agents_fastapi_endpoint(app, agent, "/agent") ``` -Need finer control (a custom transport, your own SSE framing, per-run -branching)? Use the translator directly — see the section above. +This registers: -## API overview +- `POST /agent` — accepts `RunAgentInput` and streams encoded AG-UI events. +- `GET /agent/health` — returns the wrapped agent's name and health status. -Everything you need is importable from the package root: +Pass `include_health=False` if your application already owns health checks. +The helper adds routes; it does not start a server, store sessions, or manage +authentication. + +## Public API ```python from ag_ui_openai_agents import ( AGUITranslator, OpenAIAgentsAgent, - add_openai_agents_fastapi_endpoint, TranslatedInput, + add_openai_agents_fastapi_endpoint, ) ``` -| Name | Kind | Use it for | -|---|---|---| -| `AGUITranslator` | translator (recommended) | compose it yourself — `to_openai` + `to_agui`; full control of the agent and server | -| `TranslatedInput` | result type | what `translator.to_openai(...)` returns — `messages`/`tools` plus passthrough fields | -| `OpenAIAgentsAgent` | wrapper class | serve an agent: `run_streamed(RunAgentInput) -> AsyncIterator[BaseEvent]` | -| `add_openai_agents_fastapi_endpoint(app, agent, path)` | helper | wire a wrapped agent to FastAPI (SSE + `/health`) | - -The wrapper is built on the translator; it trades control for less code. The -translator is just an events translator — it does not own your agent or your -server, so start there unless the wrapper's shortcut fits as-is. - -Everything else (`AGUIToOpenAITranslator`, `OpenAIToAGUITranslator`, -`ClientToolPending`, and the per-type `translate_*` override points) is the -advanced engine layer — import it from `ag_ui_openai_agents.engine`, not the -package root; see [Advanced: the engine layer](#advanced-the-engine-layer). - -`AGUITranslator` pairs with the SDK's streaming run mode: - -| Class | Pairs with | `to_agui(...)` returns | -|---|---|---| -| `AGUITranslator` | `Runner.run_streamed` | async iterator of AG-UI events, live | - -It is stateless and reusable — each `to_agui` call internally creates the -fresh per-run engine it needs. Create one instance and share it. - -### Choose a Pattern - -| Need | Use | -|---|---| -| Full control of the agent config and the server (recommended default) | `AGUITranslator` — compose it yourself | -| Just serve a fixed agent over FastAPI, no custom server logic | `OpenAIAgentsAgent` + `add_openai_agents_fastapi_endpoint` | -| Live chat, tool-call progress, reasoning progress | `AGUITranslator` with `Runner.run_streamed` | -| FastAPI SSE | Return `StreamingResponse(_stream(...), media_type="text/event-stream")` | -| WebSocket or another async transport | Iterate `translator.to_agui(result, run_input)` and send each event JSON | -| Custom model settings, tracing, guardrails, handoffs | Pass normal OpenAI Agents SDK args to `Runner.run_streamed` | -| Custom AG-UI mapping behavior | Subclass an engine translator and inject it into the public translator | - -## Public API reference - ### `AGUITranslator` -`AGUITranslator` is the primary API. It is stateless and reusable: create one -instance for the application, translate each request with `to_openai`, run the -SDK normally, then translate the resulting stream with `to_agui`. It does not -own your SDK agent, server routes, authentication, or session storage. +The main, stateless public API. One instance can serve concurrent requests. ```python translator = AGUITranslator() -translated_input = translator.to_openai(run_input) -result = Runner.run_streamed(agent, input=translated_input.messages) - -async for event in translator.to_agui(result, run_input): - yield encoder.encode(event) ``` -#### Constructor - -```python -AGUITranslator(*, inbound_cls=AGUIToOpenAITranslator, outbound_cls=OpenAIToAGUITranslator) -``` - -These are advanced extension points for changing one mapping without forking -the public orchestration. Both defaults live in `ag_ui_openai_agents.engine`, -not the package root: - -| Parameter | Default | Meaning | -|---|---|---| -| `inbound_cls` | `AGUIToOpenAITranslator` | Class used for AG-UI request → SDK input translation. One instance is reused because it is stateless. | -| `outbound_cls` | `OpenAIToAGUITranslator` | Class used for SDK stream → AG-UI event translation. A fresh instance is created for every run because it tracks open stream windows. | - -For normal use, pass neither parameter. For a custom mapping, subclass the -relevant engine class (`from ag_ui_openai_agents.engine import -AGUIToOpenAITranslator, OpenAIToAGUITranslator`); see -[Advanced: the engine layer](#advanced-the-engine-layer). - #### `to_openai(run_input)` -```python -translated_input = translator.to_openai(run_input) -``` - -Accepts one `RunAgentInput` and returns `TranslatedInput`: +Converts one `RunAgentInput` into `TranslatedInput`: -| Field | Use | +| Field | Behavior | |---|---| -| `messages` | Responses API items for `Runner.run_streamed(input=...)`. | -| `tools` | Client-owned `FunctionTool` proxies. Clone the agent and merge these tools for this request. | -| `state`, `context`, `forwarded_props`, `thread_id`, `run_id`, `parent_run_id`, `resume` | Original request data, preserved for your application. | +| `messages` | Converted to the OpenAI Agents SDK's input format. | +| `tools` | Converted to request-scoped OpenAI Agents SDK `FunctionTool` proxies. | +| `thread_id`, `run_id`, `parent_run_id` | Preserved unchanged. | +| `state`, `context`, `forwarded_props`, `resume` | Preserved for application orchestration. | -`TranslatedInput` does not run an agent and does not put `state` or `context` -into prompts automatically. +`to_openai` does not run the agent. It also does not automatically put `state`, +`context`, or `forwarded_props` into the model prompt. #### `to_agui(events, run_input, ...)` +Accepts either the OpenAI Agents SDK's `RunResultStreaming` or its `stream_events()` async +iterator and yields AG-UI `BaseEvent` objects. + ```python async for event in translator.to_agui(result, run_input): - yield encoder.encode(event) + ... ``` -`events` accepts either the `RunResultStreaming` returned by -`Runner.run_streamed` or its `stream_events()` iterator. `run_input` supplies -the lifecycle IDs and message history for snapshots. +| Option | Default | Purpose | +|---|---:|---| +| `start_custom_event` | `None` | Emit a `CustomEvent` after `RUN_STARTED`. | +| `initial_state` | `None` | Emit an initial `STATE_SNAPSHOT`. | +| `final_state` | `None` | Emit a final `STATE_SNAPSHOT` after streaming. | +| `emit_messages_snapshot` | `True` | Emit the complete message history before finishing. | +| `end_custom_event` | `None` | Emit a `CustomEvent` immediately before `RUN_FINISHED`. | +| `emit_run_error` | `True` | Emit `RUN_ERROR` before re-raising ordinary errors. | +| `run_error_message` | `None` | Replace exception text with a client-safe message. | -| Parameter | Default | Meaning | -|---|---|---| -| `start_custom_event` | `None` | A `CustomEvent` sent after `RUN_STARTED`; its value may be static or a sync/async factory. | -| `initial_state` | `None` | Static, sync, or async source for an initial `STATE_SNAPSHOT`. | -| `final_state` | `None` | Static, sync, or async source for a final `STATE_SNAPSHOT`. | -| `emit_messages_snapshot` | `True` | Add `MESSAGES_SNAPSHOT` before the terminal event. | -| `end_custom_event` | `None` | A `CustomEvent` sent after the snapshots and before `RUN_FINISHED`; its value may be static or a sync/async factory. | -| `emit_run_error` | `True` | Send `RUN_ERROR` for an ordinary lifecycle error, then re-raise it. | -| `run_error_message` | `None` | Client-safe error text; by default the event uses `str(exception)`. | - -`initial_state` and `final_state` can each be a value, a zero-argument -function, or a zero-argument async function. A supplied `None` skips that -snapshot; an empty `{}` is still a valid snapshot. Custom events are passed as -complete `CustomEvent` objects. Their `value` may use the same static, sync, or -async forms and is resolved immediately before the event is emitted. - -Successful runs follow this order: - -```text -RUN_STARTED -start_custom_event (optional) -STATE_SNAPSHOT (initial, optional) -… streamed step, text, tool, and reasoning events … -close any open stream windows -STATE_SNAPSHOT (final, optional) -MESSAGES_SNAPSHOT (optional, enabled by default) -end_custom_event (optional) -RUN_FINISHED -``` - -If lifecycle processing raises an ordinary exception, the translator emits -`RUN_ERROR` when enabled and re-raises it. It does not emit final state, a -messages snapshot, `end_custom_event`, or `RUN_FINISHED` on that error path. +State sources and custom-event values may be static values, zero-argument +functions, or zero-argument async functions. They are resolved at their +documented lifecycle position. `None` skips a state snapshot; `{}` emits one. ### `OpenAIAgentsAgent` -`OpenAIAgentsAgent` is the convenience wrapper for a fixed SDK `Agent`. -Internally it performs the same `to_openai` → `Runner.run_streamed` → `to_agui` -flow shown above. Use it when that standard flow is enough; use -`AGUITranslator` directly when your endpoint needs custom branching, -orchestration, or transport behavior. +A convenience wrapper around a fixed OpenAI Agents SDK `Agent`: ```python -wrapped_agent = OpenAIAgentsAgent( - Agent(name="assistant", instructions="Be concise."), +wrapped = OpenAIAgentsAgent( + agent, + name="public-name", + description="Optional description", + run_config=run_config, + initial_state=lambda: load_state(), + final_state=lambda: load_state(), + run_error_message="Agent run failed", ) -``` - -| Constructor parameter | Default | Meaning | -|---|---|---| -| `agent` | required | SDK `Agent` to run. | -| `name` | `agent.name` | Public name returned by the helper health route. | -| `description` | `""` | Optional application metadata. | -| `translator` | new `AGUITranslator` | Translator instance, including any custom engine classes. | -| `run_config` | `None` | `RunConfig` passed to every `Runner.run_streamed` call. | -| `start_custom_event` | `None` | A `CustomEvent` whose static or dynamic value is resolved after `RUN_STARTED`. | -| `initial_state` | `None` | Same state-source forms as `AGUITranslator.to_agui`. | -| `final_state` | `None` | Same state-source forms as `AGUITranslator.to_agui`. | -| `emit_messages_snapshot` | `True` | Forwarded to `to_agui`. | -| `end_custom_event` | `None` | A `CustomEvent` whose static or dynamic value is resolved before the terminal event. | -| `emit_run_error` | `True` | Forwarded to `to_agui`. | -| `run_error_message` | `None` | Forwarded to `to_agui`. | - -Call `await` through its async iterator with `run_streamed(run_input)`. It yields -`BaseEvent` objects; you encode or transport them yourself unless you use the -FastAPI helper. Client-owned tools are merged onto a clone for that run, so a -tool declared by one request never changes the shared SDK agent or leaks into -another request. - -### `add_openai_agents_fastapi_endpoint` -`add_openai_agents_fastapi_endpoint` connects an `OpenAIAgentsAgent` to a -FastAPI app. It is the highest-level option: it owns HTTP POST handling, AG-UI -SSE encoding, and an optional health endpoint, but not your agent's own behavior. - -```python -app = FastAPI() -add_openai_agents_fastapi_endpoint(app, wrapped_agent, "/assistant") +async for event in wrapped.run_streamed(run_input): + ... ``` -| Parameter | Default | Meaning | -|---|---|---| -| `app` | required | FastAPI application to register routes on. | -| `agent` | required | `OpenAIAgentsAgent` to execute for incoming requests. | -| `path` | `"/"` | POST route that accepts `RunAgentInput`. | -| `include_health` | `True` | Whether to register `GET {path}/health`. | +The wrapper: + +- translates the request; +- adds non-conflicting client tools to a per-request agent clone; +- passes `translated.context` to `Runner.run_streamed(context=...)`; +- runs the OpenAI Agents SDK in streaming mode; +- translates the result and emits lifecycle/snapshot events. -For `path="/assistant"`, the helper registers: +The wrapper exposes the same lifecycle controls as `AGUITranslator.to_agui()`: -| Route | Behavior | +| Option | Purpose | |---|---| -| `POST /assistant` | Runs the wrapper and returns encoded AG-UI SSE events. | -| `GET /assistant/health` | Returns `{"status": "ok", "agent": {"name": ...}}`. | +| `start_custom_event` | Emit a custom event after `RUN_STARTED`. | +| `initial_state` | Emit the initial `STATE_SNAPSHOT`. | +| `final_state` | Emit the final `STATE_SNAPSHOT`. | +| `emit_messages_snapshot` | Enable or disable `MESSAGES_SNAPSHOT`. | +| `end_custom_event` | Emit a custom event before `RUN_FINISHED`. | +| `emit_run_error` | Enable or disable `RUN_ERROR`. | +| `run_error_message` | Set the client-visible `RUN_ERROR.message`. | -Pass `include_health=False` when the application owns its health route. +It does not expose the `RunResultStreaming` object. Use the translator directly +when a custom event must inspect the run result, or when you need custom +orchestration. -The helper uses the request `Accept` header when creating `EventEncoder`, so -its response content type matches AG-UI content negotiation. It adds routes -only; it does not start a server or manage sessions. +The shared agent is never mutated. If a client tool has the same name as a +server tool, the server tool wins and the client tool is ignored with a warning. -### Streaming: Live AG-UI Output +Use `AGUITranslator` directly when you need the `RunResultStreaming`, custom +branching, resume logic, a different transport, or per-request OpenAI Agents SDK options. -AG-UI is an ordered event stream by design, so streaming is the primary mode: +### `add_openai_agents_fastapi_endpoint` ```python -translator = AGUITranslator() +add_openai_agents_fastapi_endpoint( + app, + wrapped_agent, + path="/agent", + include_health=True, +) +``` -translated_input = translator.to_openai(run_input) -result = Runner.run_streamed(agent, input=translated_input.messages) +The helper respects the request's `Accept` header through AG-UI's +`EventEncoder` and returns the matching streaming content type. -async for event in translator.to_agui(result, run_input): - ... # AG-UI BaseEvent, ready to encode -``` +## What happens during a run -You may also pass the SDK event iterator directly: +1. `to_openai` translates message history and client-declared tools. +2. Your code—or `OpenAIAgentsAgent`—starts `Runner.run_streamed`. +3. A fresh outbound engine is created for that run. +4. `RUN_STARTED` is emitted using the request's IDs. +5. OpenAI Agents SDK events are correlated into AG-UI text, reasoning, tool, and step windows. +6. Any open windows are closed when the OpenAI Agents SDK stream ends. +7. Optional final state and the default `MESSAGES_SNAPSHOT` are emitted. +8. The run ends with `RUN_FINISHED`, or `RUN_ERROR` for an ordinary failure. -```python -async for event in translator.to_agui(result.stream_events(), run_input): - ... +Successful lifecycle order: + +```text +RUN_STARTED +start_custom_event optional +STATE_SNAPSHOT optional initial state +... STEP / REASONING / TEXT / TOOL events ... +STATE_SNAPSHOT optional final state +MESSAGES_SNAPSHOT enabled by default +end_custom_event optional +RUN_FINISHED ``` -`to_agui` handles the streaming bookkeeping for the run. If the SDK stream ends -while text, tool-call arguments, or reasoning output is still open, the -translator emits the matching close event before the iterator finishes. It -always wraps the whole stream with `RUN_STARTED` (first) and -`RUN_FINISHED`/`RUN_ERROR` (last) — see [Lifecycle Events](#lifecycle-events). +On an ordinary error, open event windows are closed, `RUN_ERROR` is emitted +when enabled, and the original exception is re-raised for server logging. +Final state, messages snapshot, end custom event, and `RUN_FINISHED` are not +emitted on the error path. -All normal OpenAI Agents SDK run options stay on the SDK call: +The client-visible error text is configurable with `run_error_message`: ```python -result = Runner.run_streamed( - agent, - input=translated_input.messages, - context=my_context, - max_turns=8, - run_config=run_config, -) - -async for event in translator.to_agui(result, run_input): - ... +async for event in translator.to_agui( + result, + run_input, + run_error_message="The agent could not complete this request.", +): + yield encoder.encode(event) ``` -### What `to_openai` gives you - -`TranslatedInput` mirrors `RunAgentInput` field for field: +This changes `RUN_ERROR.message` only. The original exception is still raised +after the event so the server can log the detailed failure. Use +`run_error_message` in production to keep internal exception details out of the +client response while preserving the original exception for server logs. + +If a consumer stops reading early and passed a `RunResultStreaming`, the + translator cancels that owned OpenAI Agents SDK run. If the caller passes only a bare async +iterator, cancellation remains the caller's responsibility. + +## Feature support + +| Feature | Support | Notes | +|---|---:|---| +| Streaming assistant text and refusals | Yes | Emits one ordered text window per OpenAI Agents SDK message item. | +| Backend function tools | Yes | Tool call arguments and results are streamed. | +| Hosted tools | Yes | OpenAI Agents SDK hosted tools are surfaced as tool calls; some do not expose streamed arguments. | +| Frontend/client-owned tools | Yes | Request tools become OpenAI Agents SDK `FunctionTool` proxies and complete in a later AG-UI request. | +| Handoffs | Yes | Handoff calls/results map to tools; agent changes map to steps. | +| Agents as tools | Yes | Nested specialist calls appear as ordinary tool calls/results. | +| Reasoning | Yes | Summary/text streams; encrypted content supports replay. | +| Guardrails | Yes | OpenAI Agents SDK tripwire exceptions surface through `RUN_ERROR`. | +| State snapshots | Yes | Application supplies initial/final state sources. | +| Messages snapshot | Yes | Enabled by default with stable streamed IDs. | +| Custom lifecycle events | Yes | Start/end `CustomEvent` hooks. | +| Multimodal input | Partial | Text, images, documents, and some audio; see below. | +| Multimodal output | No | AG-UI currently has no matching image/audio output event. | +| Backend approval | Example | Requires application-owned `RunState` persistence and resume logic. | +| MCP approval request | Yes | Emitted as `CUSTOM` named `mcp_approval_request`. | + +## Message mapping: AG-UI → OpenAI + +`to_openai` keeps message order. One AG-UI message may produce multiple OpenAI Agents SDK +items, especially an assistant message containing tool calls. + +| AG-UI input | OpenAI Agents SDK input | ID behavior | +|---|---|---| +| `SystemMessage` | `message`, role `system` | AG-UI message ID is not sent. | +| `DeveloperMessage` | `message`, role `developer` | AG-UI message ID is not sent. | +| `UserMessage` | `message`, role `user`, with translated content parts | AG-UI message ID is not sent. | +| `AssistantMessage.content` | Assistant input message | AG-UI message ID is not sent; empty text is omitted. | +| `AssistantMessage.tool_calls[]` | One `function_call` per tool call | `ToolCall.id → call_id` unchanged. | +| `ToolMessage` | `function_call_output` | `tool_call_id → call_id` unchanged. | +| `ReasoningMessage` | `reasoning` item | `message.id → reasoning.id`; emitted only with `encrypted_value`. | +| `ActivityMessage` | Not mapped | Dropped with a debug log. | +| `RunAgentInput.tools[]` | OpenAI Agents SDK `FunctionTool` proxies | Tool name and JSON Schema are preserved. | -| AG-UI field | Lands in | -|---|---| -| `messages` | `translated_input.messages` — Responses-API input items for `Runner.run_streamed` | -| `tools` | `translated_input.tools` — SDK `FunctionTool` proxies for client-declared tools; merge with `agent.clone(tools=[*agent.tools, *translated_input.tools])` | -| `state`, `context`, `forwarded_props` | passthrough — the direct translator never injects them into model input; use them in your application as needed | -| `thread_id`, `run_id`, `parent_run_id`, `resume` | passthrough | +The Responses input format has no `tool` role. Tool history is represented by +`function_call` and `function_call_output` items joined by the same `call_id`. -The most important field is `translated_input.messages`; pass it as `input=` to -`Runner.run_streamed`. +Unknown message types are skipped rather than failing the entire request. -If `translated_input.tools` is non-empty, merge those tools into the agent for this -request: +## Event mapping: OpenAI → AG-UI -```python -run_agent = agent -if translated_input.tools: - run_agent = agent.clone(tools=[*agent.tools, *translated_input.tools]) -``` +| OpenAI Agents SDK source | AG-UI output | +|---|---| +| Message item plus text/refusal deltas | `TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT* → TEXT_MESSAGE_END` | +| Function, hosted-tool, or handoff call | `TOOL_CALL_START → TOOL_CALL_ARGS* → TOOL_CALL_END` | +| Function/handoff output | `TOOL_CALL_RESULT` | +| Reasoning summary/text | `REASONING_START → REASONING_MESSAGE_START → REASONING_MESSAGE_CONTENT* → REASONING_MESSAGE_END → REASONING_END` | +| Reasoning encrypted content | `REASONING_ENCRYPTED_VALUE` | +| Agent update/handoff target | `STEP_FINISHED` for the previous agent, then `STEP_STARTED` | +| MCP approval request | `CUSTOM` with name `mcp_approval_request` | +| MCP tool listing/approval response | Not emitted; server bookkeeping only. | +| Run start/success/failure | `RUN_STARTED`, `RUN_FINISHED`, or `RUN_ERROR` | + +`response.output_text.done` closes one text part, not necessarily the complete +message item. The translator therefore closes a text window on the item-level +done event, the completed run item, or finalization. This keeps multi-part text +under one AG-UI message ID. + +## ID mapping and guarantees + +| Source | AG-UI ID | +|---|---| +| OpenAI Agents SDK message item `id` | `message_id` | +| Function/hosted tool `call_id` | `tool_call_id` | +| Hosted tool without `call_id` | Item `id`, then generated `call_...` fallback | +| Tool result `call_id` | `tool_call_id`; result `message_id` is `-result` | +| Reasoning item `id` | Reasoning phase `message_id` | +| Additional reasoning parts | `-1`, `-2`, ... | +| AG-UI request `thread_id` / `run_id` | Same lifecycle event fields, unchanged | -A tool's `parameters` pass through as its JSON Schema. `"type": "object"` is -optional there — if it's missing it gets added, and the declared fields are -kept as-is. Only a `None`/empty spec becomes an empty (parameter-less) schema. +Rules: -### Lifecycle Events +- A real OpenAI Agents SDK ID always wins. +- If the OpenAI Agents SDK omits an ID or uses its placeholder ID, the integration generates + a stable ID for that item (`msg_...`, `call_...`, or `rs_...`). +- One resolved ID is reused for every start/content/end event in that window. +- The same IDs are reused in `MESSAGES_SNAPSHOT`, preventing duplicate client + messages when streamed output is reconciled with final history. +- Existing input history keeps its original AG-UI IDs in the snapshot. +- Reasoning is intentionally excluded from `MESSAGES_SNAPSHOT`; encrypted + reasoning is carried by `REASONING_ENCRYPTED_VALUE` instead. +- Internal output-index correlation keys never appear on AG-UI events. -`to_agui` starts ordinary runs with `RUN_STARTED` and ends them with -`RUN_FINISHED`, or with `RUN_ERROR` when lifecycle processing raises an -ordinary exception. The original exception is then re-raised for application -logging. `RUN_STARTED` and `RUN_FINISHED` are not configurable: +## Messages and state snapshots -```python -async for event in translator.to_agui(result, run_input): - yield encoder.encode(event) -``` +`MESSAGES_SNAPSHOT` is emitted before `RUN_FINISHED` by default. It contains: -`thread_id`/`run_id` come straight off `run_input` — no separate params to -pass, since `run_input` is already required for the lifecycle events (and, -by default, the snapshot). +1. the original `RunAgentInput.messages` with their existing IDs; and +2. assistant messages, tool calls, and tool results produced during this run, + using the same IDs already streamed to the client. -The `RUN_ERROR` on the error path is tunable. By default it carries -`str(exc)`; pass `run_error_message` to send a fixed string instead, so raw -exception text never reaches the client — the real exception is still -re-raised, so your own logging keeps it: +Disable it only when your application emits its own authoritative history: ```python async for event in translator.to_agui( - result, run_input, run_error_message="Agent run failed" + result, + run_input, + emit_messages_snapshot=False, ): - yield encoder.encode(event) + ... ``` -Pass `emit_run_error=False` only if an outer handler emits the terminal error; -otherwise an ordinary exception re-raises without `RUN_ERROR`. +State remains application-owned. Supply snapshots explicitly: -Open windows are closed before the error goes out: a run that dies while an -assistant message is streaming still gets its `TEXT_MESSAGE_END` (and any -open tool-call, reasoning, or step end) ahead of `RUN_ERROR`, so a client -that keys its teardown on the `*_END` events doesn't leave the half-streamed -message spinning forever. +```python +state = dict(run_input.state or {}) +initial_state = dict(state) -### Client Disconnects +result = Runner.run_streamed( + agent, + input=translated.messages, + context=state, +) -If the client goes away, the server stops iterating `to_agui` and `to_agui` -cancels the SDK run for you (`RunResultStreaming.cancel()`) — the run is a -separate task, and left alone it keeps calling the model and running tools for -a client that will never see the result. Generator closure and task -cancellation emit no further events because the client cannot receive them. -Any incomplete exit still cancels the owned run. +async for event in translator.to_agui( + result, + run_input, + initial_state=initial_state, + final_state=lambda: dict(state), +): + ... +``` -This only applies when you hand `to_agui` the `RunResultStreaming` object: +The final factory runs after streaming, so it can observe changes made by tools +or hooks. -```python -result = Runner.run_streamed(agent, input=translated_input.messages) -async for event in translator.to_agui(result, run_input): # cancelled on disconnect - yield encoder.encode(event) -``` +## Context and forwarded properties -Pass a bare `stream_events()` iterator instead and the run stays yours to -cancel — `to_agui` won't end something it wasn't given: +`state`, `context`, `forwarded_props`, and `resume` pass through on +`TranslatedInput`; their meaning remains application-specific. -```python -result = Runner.run_streamed(agent, input=translated_input.messages) -try: - async for event in translator.to_agui(result.stream_events(), run_input): - yield encoder.encode(event) -finally: - result.cancel() # your iterator, your cleanup -``` +Be aware of two uses of “context”: -### Messages Snapshot +- `RunAgentInput.context` is the AG-UI list sent by the client. +- `Runner.run_streamed(context=...)` is the OpenAI Agents SDK dependency object + available through `RunContextWrapper`. -`MESSAGES_SNAPSHOT` lets the client resync its whole message list in one -event — it reconciles by message id, fixing anything the granular stream -couldn't express (a reload mid-conversation, history rewritten by a handoff -input filter, a dropped connection). `to_agui` appends one by default, -right after the stream's own flush and just before `RUN_FINISHED`, using -`run_input.messages` for the prior turns: +The convenience wrapper passes the AG-UI context list into the OpenAI Agents SDK context +slot. This enables dynamic instructions or tools to read the list, as shown by +the `dynamic_system_prompt` example. With the direct translator, you may pass +that list, transform it into prompt text, or replace it with your own dependency +object. -```python -async for event in translator.to_agui(result, run_input): - yield encoder.encode(event) -``` +The inbound engine's `translate_context()` method renders AG-UI context as +`description: value` lines when you want to add it to instructions. -The snapshot is `run_input.messages` (untouched, keeping the ids the client -already renders) plus this run's messages, built inline as the engine -streams — each message's id is resolved once and handed to both the -streamed event and the snapshot entry, so they can never disagree, even on -backends that don't stamp real ids. Reasoning items -are not included; the client keeps its streamed reasoning bubbles through -the merge on its own. If the run raises, `to_agui` yields `RUN_ERROR` and -re-raises before the snapshot line runs — nothing is emitted on the error -path. - -> `run_input.messages` passes through untouched — filter it first if it -> holds anything the client shouldn't see echoed back, e.g. a system -> prompt sent as history instead of via `agent.instructions`: -> ```python -> filtered = run_input.model_copy( -> update={"messages": [m for m in run_input.messages if m.role != "system"]} -> ) -> async for event in translator.to_agui(result, filtered): -> ... -> ``` - -Pass `emit_messages_snapshot=False` to opt out (e.g. you assemble your own). -Auto-emission works the same whether `to_agui` is given the -`RunResultStreaming` object or a bare `result.stream_events()` iterator — -the snapshot is built from the engine's own state (collected as it -streamed), not `result.new_items`, so there's nothing the bare-iterator -form is missing. - -### What Your Code Still Owns - -The translator translates. Your run loop still owns orchestration: - -| Concern | Owned by | -|---|---| -| `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` | `to_agui` (always) | -| `STATE_SNAPSHOT` | `to_agui` when you provide `initial_state` and/or `final_state` | -| `MESSAGES_SNAPSHOT` | `to_agui` (on by default; pass `emit_messages_snapshot=False` to own it yourself) | -| Session storage and thread history | Your server | -| SSE, WebSocket, HTTP response shape | Your server/framework | -| OpenAI agent choice, model settings, handoffs, guardrails | Your OpenAI Agents SDK code | -| AG-UI message/tool/event shape conversion | This package | +## Tools and human-in-the-loop -This keeps the integration framework-neutral. FastAPI, Starlette, Django, -aiohttp, raw ASGI, WebSockets, or tests can all use the same translator calls. +### Backend tools -### Message, Event, and ID Mapping +Normal OpenAI Agents SDK `@function_tool` tools stay on the server. Their calls and results +become AG-UI tool events without special integration code. -Source of truth: `engine/agui_to_openai.py` (inbound) and -`engine/openai_to_agui.py` (outbound). +### Frontend-owned tools -#### Inbound: AG-UI → OpenAI SDK (`to_openai`) +Tools declared in `RunAgentInput.tools` belong to the client. `to_openai` +creates OpenAI Agents SDK `FunctionTool` proxies so the model can call them, but the frontend +must execute them. -| AG-UI message | OpenAI SDK input item | AG-UI ID in → OpenAI ID out | -|---|---|---| -| `UserMessage` | `message` (role `user`) | `message.id` dropped, not sent. All-unsupported content parts (e.g. video-only) drop the whole message. | -| `SystemMessage` | `message` (role `system`) | `message.id` dropped, not sent. | -| `DeveloperMessage` | `message` (role `developer`) | `message.id` dropped, not sent. | -| `AssistantMessage` | `{"role": "assistant", ...}` (if text) + one `function_call` per tool call | `message.id` dropped. **`ToolCall.id` → `function_call.call_id`** (preserved 1:1). Empty text emits no item. | -| `ToolMessage` | `function_call_output` | `message.id` dropped. **`tool_call_id` → `function_call_output.call_id`** (preserved 1:1). | -| `ReasoningMessage` | `reasoning` item | **`message.id` → `reasoning.id`** (preserved 1:1), only when `encrypted_value` is set — plaintext-only reasoning is dropped, no item emitted. | -| `ActivityMessage` | *(dropped)* | No SDK equivalent; dropped with a debug log, no ID involved. | -| `RunAgentInput.tools` | `FunctionTool` proxy | No ID — tool `name` + JSON Schema pass through. | - -#### Outbound: OpenAI SDK → AG-UI (`to_agui`) - -| OpenAI SDK item / event | AG-UI events | OpenAI ID in → AG-UI ID out | -|---|---|---| -| `message` item + `output_text`/`refusal` deltas | `TEXT_MESSAGE_START` / `CONTENT` / `END` | **item `id` → `message_id`** if real, else generate `msg_`. Same ID reused in `MESSAGES_SNAPSHOT`. | -| `function_call`, hosted-tool call, or handoff call | `TOOL_CALL_START` / `ARGS` / `END` | **`call_id` → `tool_call_id`** first choice; falls back to item `id`, then generated `call_`. Same ID reused in the snapshot. | -| `function_call_output` or handoff output | `TOOL_CALL_RESULT` | **`call_id` → `tool_call_id`** (passthrough). `message_id` is *derived*, not from the wire: `-result`. Skipped entirely if `call_id` is missing. | -| `reasoning` item + summary/reasoning-text deltas | `REASONING_START`, `REASONING_MESSAGE_START`/`CONTENT`/`END` per part, `REASONING_END` | **item `id` → phase `message_id`** if real, else generate `rs_`. First part reuses the phase ID; later parts get `-1`, `-2`, … (derived). Not included in `MESSAGES_SNAPSHOT`. | -| `reasoning.encrypted_content` | `REASONING_ENCRYPTED_VALUE` (subtype `message`) | **phase `message_id` → `entity_id`** (reused, not new). Emitted at most once per reasoning item. | -| `AgentUpdatedStreamEvent` (first agent, each handoff target) | `STEP_FINISHED` (previous) then `STEP_STARTED` | No ID at all — `step_name` is the agent's `name`, or `"agent"` if unnamed. | -| `MCPApprovalRequestItem` | `CUSTOM` named `mcp_approval_request` | No AG-UI ID assigned; `value` carries the raw request as-is. | -| `MCPListToolsItem`, `MCPApprovalResponseItem` | *(dropped)* | Server-side bookkeeping; dropped with a debug log, no ID involved. | -| Run input / stream completion / error | `RUN_STARTED`, then `RUN_FINISHED`/`RUN_ERROR`; optional `STATE_SNAPSHOT`; `MESSAGES_SNAPSHOT` by default | **`RunAgentInput.thread_id`/`run_id` → same fields on the lifecycle events** (passthrough, not generated). | - -Text windows close on **item-level** signals only. `response.output_text.done` -ends one content *part*, not the message — a message can carry several parts -(multiple text blocks, or text followed by a refusal), so the translator -ignores it and keeps streaming later parts into the same window under the same -`message_id`. `TEXT_MESSAGE_END` is emitted by whichever of these arrives -first: `response.output_item.done`, the run-item commit (for backends that -skip the raw done), or `finalize()` (stream ended mid-message). Closing on the -part-level done would split one wire message into several AG-UI messages, the -later ones under generated IDs the snapshot could not merge. - -#### ID rules that apply everywhere - -- **Real wire ID always wins.** Generated only when the SDK sends none, or - sends its `FAKE_RESPONSES_ID` placeholder (some non-Responses backends - stamp every item with it). -- **Generated IDs mimic wire prefixes** (`msg_`, `call_`, `rs_`) so a - client can't tell generated from real. -- **A hyphen marks an ID this package derived** (`-result`, - `-1`) — wire IDs never contain one. -- **Every streamed ID is reused verbatim in `MESSAGES_SNAPSHOT`** — one - resolution per item, so the streamed event and the snapshot entry can never - disagree. Reasoning is the one item type excluded from the snapshot. -- **Internal-only correlation key:** raw response events key their open - windows by the real item ID, or by `__idx_` when the ID is - missing/placeholder. That key is bookkeeping — it is never put on an AG-UI - event. -- Unknown SDK message/event types are dropped with a debug log instead of - failing the run. - -### Guardrails - -The AG-UI protocol has no guardrail event type, so guardrails surface as -run errors. When an input or output guardrail trips, the OpenAI Agents SDK -raises `InputGuardrailTripwireTriggered` / `OutputGuardrailTripwireTriggered` -mid-run; that exception flows through the stream, `to_agui` yields -`RUN_ERROR`, and re-raises. No special handling needed — a tripwire aborts -the run like any other error. - -Guardrail messages can be noisy or leak your policy internals, so this is a -natural place for `run_error_message` — send a clean, client-safe string -while your logs keep the real exception: +Recommended flow: + +1. Merge translated client tools onto a per-request agent clone. +2. Configure OpenAI Agents SDK `StopAtTools` for those names when the run should stop as soon + as the model calls one. +3. Render/execute the tool in the client. +4. Send its result as a `ToolMessage` in the next AG-UI request. +5. `to_openai` converts that result to `function_call_output`, preserving the + `call_id`, and the agent continues from the conversation history. ```python -async for event in translator.to_agui( - result, run_input, run_error_message="Request blocked by content policy" -): - yield encoder.encode(event) +from agents import Agent, StopAtTools + +agent = Agent( + name="planner", + instructions="Use generate_steps when asked to make a plan.", + tool_use_behavior=StopAtTools(stop_at_tool_names=["generate_steps"]), +) ``` -### Multi-modality +This pattern covers frontend tool rendering, tool-based generative UI, and +client-owned human approval. -**Input (what the user sends the agent):** an AG-UI `UserMessage` can carry -more than text — images, audio clips, documents, etc. Each one is a typed -content part (`ag_ui.core.InputContent`). The translator converts each part -into the block shape the OpenAI Agents SDK expects for that message: +### Backend tool approval -| User sends this... | SDK shape | Transport support | -|---|---|---| -| Plain text | `input_text` | Responses and Chat Completions | -| An image | `input_image`; URL passes through and inline data becomes a base64 `data:` URL | Responses and Chat Completions | -| An audio clip | `input_audio` with base64 data and a format tag | Chat Completions with an audio-capable model; not Responses | -| A document (PDF, etc.) | `input_file`; URL uses `file_url` and inline data uses base64 `file_data` | Inline data works with both; URL works with Responses only | -| A video | *(dropped, see below)* | Neither Responses nor Chat Completions | -| `BinaryInputContent` (deprecated, legacy catch-all) | selected from its mime type, e.g. `image/*` becomes `input_image` | Depends on the selected content type | - -Base64 represents binary bytes as text that can be embedded safely in JSON. It -is encoding, not encryption or compression, and adds roughly 33% size overhead. -AG-UI data sources already contain base64: the translator wraps image and file -data in `data:;base64,...` URLs, while audio uses the base64 value with a -separate format tag. - -**Video isn't supported as an agent model input.** Neither Responses nor Chat -Completions has a video content part to translate into. OpenAI's specialized -Videos API is separate from these agent inputs. Until support is added, video -parts are silently dropped (logged, not an error). To work around it, override -`translate_video_content` in a subclass — e.g. extract a few frames and send -them as images instead. - -**Output (what the agent sends back):** text only. If the agent generates an -image, audio, or speaks out loud (TTS), none of that reaches the AG-UI -client — the AG-UI protocol itself has no event type for "here's an image/audio -clip the model produced," so there's nothing to translate it into. This is a -protocol-level gap, not something specific to this integration — every -AG-UI SDK has the same hole. - -### Transport Options - -For SSE, use the AG-UI SDK's own encoder — it produces one `data:` frame per -event and gives you the matching `Content-Type`: +OpenAI Agents SDK tools marked `needs_approval=True` are different: real backend +code exists, but the OpenAI Agents SDK must pause before executing it. ```python -from ag_ui.encoder import EventEncoder +from agents import function_tool -encoder = EventEncoder() -async for event in translator.to_agui(result, run_input): - yield encoder.encode(event) +@function_tool(needs_approval=True) +def issue_refund(order_id: str) -> str: + return f"Refund issued for {order_id}" ``` -If you'd rather not depend on the encoder, the equivalent frame by hand is: +`result.interruptions` is known after draining the OpenAI Agents SDK stream. A production +server must: -```python -def encode_sse(event) -> bytes: - return f"data: {event.model_dump_json(by_alias=True, exclude_none=True)}\n\n".encode() -``` +1. call the OpenAI Agents SDK's `result.to_state()` method and store the + returned paused `RunState` in durable storage, keyed by your own thread/run + identity. This method is provided by the OpenAI Agents SDK; this AG-UI + integration only calls it. +2. emit an approval request before `RUN_FINISHED` (for example through + `end_custom_event`); +3. authenticate and validate the next approval decision; +4. claim the stored state exactly once; +5. call the OpenAI Agents SDK's `state.approve(item)` or `state.reject(item)`; + and +6. resume the OpenAI Agents SDK run with `Runner.run_streamed(agent, state)`. -For WebSockets, send the same JSON payload: +The AG-UI-specific part is the approval notification and transport: ```python -async for event in translator.to_agui(result, run_input): - await websocket.send_text(event.model_dump_json(by_alias=True, exclude_none=True)) -``` +state = result.to_state() # OpenAI Agents SDK +store[run_input.thread_id] = state -For tests or in-process consumers, collect events directly: +approval_event = CustomEvent( + name="approval_request", + value=[{"call_id": item.raw_item.call_id, "tool_name": item.tool_name}], +) # AG-UI event, emitted through to_agui(..., end_custom_event=approval_event) -```python -events = [event async for event in translator.to_agui(result, run_input)] +# On the next request, after validating the frontend decision: +state.approve(item) # or state.reject(item) — OpenAI Agents SDK +resumed = Runner.run_streamed(agent, state) # OpenAI Agents SDK ``` -### State, Context, and Forwarded Props +The included demo uses an in-memory store for clarity. Replace it with durable, +atomic storage before production use. See the complete +[`human_in_the_loop_approval.py`](examples/agents_examples/human_in_the_loop_approval.py) +example for the request routing, state store, custom event, and resume flow. -`state`, `context`, and `forwarded_props` are preserved on `TranslatedInput`; -the translator does not automatically insert them into model instructions. -That is deliberate, because apps use these fields differently. +## Reasoning -One naming collision to be aware of — there are two unrelated "context"s: +Visible reasoning summary/text maps to AG-UI reasoning events. Replay requires +encrypted reasoning content; plaintext reasoning alone cannot restore model +reasoning state. -- AG-UI `RunAgentInput.context` — readable `{description, value}` items meant - for the **model** (prompt material). -- OpenAI Agents SDK `context=` on `Runner.run*` — a dependency-injection slot - for **tools and hooks**; the model never sees it. - -AG-UI's `context` field belongs in the prompt (example below); the SDK's -`context=` slot stays yours for whatever your tools need. +Request encrypted content through normal OpenAI Agents SDK model settings: ```python -translated_input = translator.to_openai(run_input) - -instructions = agent.instructions -if translated_input.context: - instructions += "\n\nContext:\n" + "\n".join( - f"- {item.description}: {item.value}" for item in translated_input.context - ) +from agents import ModelSettings -run_agent = agent.clone(instructions=instructions) -result = Runner.run_streamed(run_agent, input=translated_input.messages) +settings = ModelSettings( + response_include=["reasoning.encrypted_content"], +) ``` -## Advanced: the engine layer +When present, encrypted content is emitted once as +`REASONING_ENCRYPTED_VALUE` and can return later through an AG-UI +`ReasoningMessage.encrypted_value`. -The public translator delegates to two independent, symmetric engine translators in -`ag_ui_openai_agents.engine`: +## Multimodal input -- `AGUIToOpenAITranslator` — inbound and stateless. Each request is converted - independently, so one instance can be reused. -- `OpenAIToAGUITranslator` — outbound and stateful per run. OpenAI Agents SDK - events arrive incrementally, so it remembers open text, tool-call, reasoning, - and agent-step sequences between events. +| AG-UI content | OpenAI input | Notes | +|---|---|---| +| Text | `input_text` | Supported. | +| Image URL or data | `input_image` | Data becomes a base64 data URL; detail is `auto`. | +| Document URL or data | `input_file` | Uses `file_url` or base64 `file_data`. | +| Audio data | `input_audio` | WAV/MP3 only; requires a compatible Chat Completions model. | +| Audio URL | Not mapped | Dropped. | +| Video | Not mapped | OpenAI agent model input has no matching video part. | +| Legacy `BinaryInputContent` | Image, audio, or file by MIME type | Best-effort compatibility path. | -`AGUITranslator` remains stateless and reusable. It reuses the inbound engine -and creates a fresh outbound engine for every `to_agui()` call. +Unsupported parts are skipped. If every part of a user message is unsupported, +the complete message is skipped so the OpenAI Agents SDK never receives empty content. -The public translator emits events in this order: +The integration currently emits text/tool/reasoning output only. It does not +translate generated image or audio output into AG-UI events. -```text -RUN_STARTED - → optional start event and initial state - → streamed STEP / REASONING / TEXT / TOOL sequences - → finalize open sequences - → optional final state and MESSAGES_SNAPSHOT - → optional end event - → RUN_FINISHED -``` +## Custom mappings -Within the outbound engine, each content sequence is correlated across OpenAI -Agents SDK events and emitted as `START → content/arguments → END`. +The public translator delegates to two engine classes: -### Outbound state model +- `AGUIToOpenAITranslator` — stateless inbound request mapping, reused. +- `OpenAIToAGUITranslator` — stateful outbound stream mapping, created per run. -An internal correlation key joins events for the same OpenAI Agents SDK output -item. It uses the real item ID when available and otherwise uses the item's -output position. The value stored for an open sequence is the ID already sent -to the AG-UI client, ensuring later content and closing events reuse it. - -| State group | Attributes | Why it is retained | -|---|---|---| -| Text | `_open_texts`, `_pending_text_ids`, `_closed_text_ids` | Reuse message IDs, avoid empty assistant messages on tool-only turns, and prevent completed run items from duplicating streamed text. | -| Tool calls | `_open_tool_calls`, `_seen_call_ids` | Attach streamed arguments to the correct tool call and prevent raw events and completed tool items from emitting the same call twice. | -| Reasoning | `_open_reasonings`, `_open_reasoning_parts`, `_closed_reasoning_ids` | Preserve phase and part ordering, then reconcile completed reasoning items without duplicate output. | -| Reasoning metadata | `_reasoning_part_seq`, `_reasoning_phase_ids`, `_emitted_encrypted_keys` | Give multiple reasoning parts stable IDs and attach encrypted replay data once, even when it arrives after visible reasoning closes. | -| Agent steps | `_current_step` | Finish the previous agent step before starting the next and close the final step when the stream ends. | -| Message snapshot | `_snapshot_messages` | Build `MESSAGES_SNAPSHOT` with the same IDs used by streamed text, tool calls, and tool results. | - -Every per-type method is a public override point. To customize one mapping, -subclass the engine and inject it — the public translator and every other mapping stay -untouched: +Their `translate_*` methods are override points. Subclass only the side you +need and inject the class into `AGUITranslator`: ```python from ag_ui_openai_agents import AGUITranslator from ag_ui_openai_agents.engine import OpenAIToAGUITranslator -class MyOutbound(OpenAIToAGUITranslator): - def translate_text_delta(self, data): - ... # your variant of one mapping - -translator = AGUITranslator(outbound_cls=MyOutbound) -``` - -## Frontend (client-owned) tools & Human-in-the-Loop - -Tools declared in `RunAgentInput.tools` belong to the **frontend** — the -browser owns their execution (rendering UI, waiting on the user), not your -server. This is the human-in-the-loop mechanism: the same one most AG-UI -integrations use, paired with CopilotKit's `useHumanInTheLoop` on the client. +class MyOutboundTranslator(OpenAIToAGUITranslator): + def translate_mcp_approval_request_item(self, item): + return [] # Application-specific behavior. -**1. Merge the client tools onto your agent per request.** `to_openai` turns -each into an SDK `FunctionTool` proxy: -```python -translated = translator.to_openai(run_input) -agent = base_agent.clone(tools=[*base_agent.tools, *translated.tools]) +translator = AGUITranslator(outbound_cls=MyOutboundTranslator) ``` -**2. Stop the run when the model calls one.** Use the SDK-native -`StopAtTools` so the run ends the instant the model emits the call — before -the (never-used) proxy body would run: +Avoid sharing an outbound engine instance: it tracks open text, tool, +reasoning, step, and snapshot state for one run. `AGUITranslator` creates the +correct fresh instance automatically. -```python -from agents import Agent, StopAtTools +## Examples -base_agent = Agent( - ..., - tool_use_behavior=StopAtTools(stop_at_tool_names=["generate_task_steps"]), -) -``` +The [`examples/`](examples/) directory contains one runnable app per supported +Dojo feature: -**3. The frontend answers; the agent resumes next request.** The tool call -streams to the browser, the user acts, and the result comes back as a -`ToolMessage` in the next run's `messages` — ordinary multi-turn history. -`to_openai` translates that `ToolMessage` into a `function_call_output`, so the -agent picks up where it left off. No custom event, no `RunState`, no -persistence to wire. +| Example | Demonstrates | +|---|---| +| `ag_ui_docs_copilot` | Agent using the integration and AG-UI documentation. | +| `agentic_chat` | Basic streaming conversation. | +| `backend_tool_rendering` | Server function call and result rendering. | +| `human_in_the_loop` | Frontend-owned tool plus user interaction. | +| `human_in_the_loop_approval` | OpenAI Agents SDK backend approval, persisted state, and resume. | +| `tool_based_generative_ui` | Frontend renders UI from tool arguments. | +| `subagents` | Agents-as-tools orchestration. | +| `custom_lifecycle_events` | Dynamic custom events using run data. | +| `dynamic_system_prompt` | Instructions derived from AG-UI context. | + +Run all examples: -``` -Request 1: user asks → agent calls generate_task_steps → RUN_FINISHED (paused) -Frontend: renders the call, user approves/edits -Request 2: messages include the ToolMessage result → agent continues +```bash +cd examples +uv sync +cp .env.example .env +# Add OPENAI_API_KEY to .env +uv run python server.py ``` -See `examples/agents_examples/human_in_the_loop.py` for the agent and -`examples/server.py` for the run loop that merges the client tools. +The server listens on `http://localhost:8024` by default. `PORT` and `HOST` +override the defaults. `GET /health` lists the mounted demos. See the +[examples guide](examples/README.md) for routes and suggested prompts. -## Backend tool approval (`needs_approval`) +## Production responsibilities -Different from the frontend-tools pattern above: here the tool is real -server-side code (`@function_tool`, `Agent.tools=[...]`), and the SDK itself -gates it with `needs_approval=True` — no AG-UI concept involved. This is for -"the backend can do this, but a human should sign off first" (e.g. issuing a -refund), as opposed to "only the browser can do this at all". +This package translates the protocol. Your application still owns: -| | Frontend tools (above) | Backend approval (here) | -|---|---|---| -| Tool implementation | None — frontend-only | Real, server-side | -| Pause mechanism | `StopAtTools`, mid-stream | `needs_approval`, only known post-stream via `result.interruptions` | -| Decision carried back as | An ordinary `ToolMessage` next turn | `forwarded_props["approval"]` + a stored `RunState` | -| Dojo demo | `human_in_the_loop` | `human_in_the_loop_approval` | +- authentication and authorization; +- API-key and secret management; +- session and message persistence; +- idempotency and concurrency control; +- approval-state storage and expiry; +- rate limits, retries, logging, and observability; +- model choice, guardrails, tools, handoffs, and OpenAI Agents SDK `RunConfig`; +- HTTP/WebSocket deployment and scaling. -**1. Mark the tool.** The SDK stops the run before the body executes and -reports the pending call on `result.interruptions`: +## Testing and local development -```python -from agents import function_tool +Clone the AG-UI repository, then work from +`integrations/openai-agents/python`: -@function_tool(needs_approval=True) -def issue_refund(order_id: str) -> str: - ... # real logic — only runs after approval +```bash +uv sync +uv run python -c "import ag_ui_openai_agents" +uv run pytest +uv build ``` -**2. `result.interruptions` is only known post-hoc — and the client drops -anything sent after `RUN_FINISHED`.** So the approval event can't be a -plain event yielded after the `to_agui()` loop; it has to be -`end_custom_event` (fires right before `RUN_FINISHED`), which means -draining the raw SDK stream yourself first instead of handing `result` -straight to `to_agui`: +`uv sync` installs the package and its development dependencies. The import +command is a quick installation smoke test, `pytest` runs the package unit +suite, and `uv build` verifies that the source distribution and wheel can be +created. No lint command is documented because this package does not currently +define a package-specific linter. -```python -raw_events = [event async for event in result.stream_events()] - -end_custom_event = None -if result.interruptions: - state = result.to_state() # serialize the paused run - store[run_input.thread_id] = state # keep it somewhere until the decision arrives - end_custom_event = CustomEvent( - name="approval_request", - value=[ - {"call_id": item.raw_item.call_id, "tool_name": item.tool_name} - for item in result.interruptions - ], - ) - -async def _replay(): - for event in raw_events: - yield event - -async for ag_event in translator.to_agui(_replay(), run_input, end_custom_event=end_custom_event): - yield encoder.encode(ag_event) -``` +The test suite covers the public translator, wrapper, FastAPI endpoint, +message/content mapping, event windows, snapshots, IDs, cancellation, tools, +reasoning, and OpenAI Agents SDK discriminator drift. Unit tests do not require a live API +key. -**3. Resume from the stored state on the next request**, once the client -sends the decision back (however you choose to carry it — e.g. -`forwarded_props`): +The runnable examples have a separate test environment and test suite: -```python -state = store.pop(run_input.thread_id) -item = next(i for i in state.get_interruptions() if i.raw_item.call_id == call_id) -state.approve(item) if approve else state.reject(item) -result = Runner.run_streamed(agent, state) # resumes, not a fresh run +```bash +cd examples +uv sync +uv run pytest ``` -There's no AG-UI-native event for the approval request — `CustomEvent` is -the same escape hatch used for MCP approval requests elsewhere in this -package. See `examples/agents_examples/human_in_the_loop_approval.py` for -the agent and `examples/server.py` (`/human_in_the_loop_approval`) for the -full hand-routed loop, including the in-memory pending-state store. - -## Gotchas - -- **Reasoning replay** (sending reasoning back to OpenAI) only works via - `encrypted_content` — set - `ModelSettings(response_include=["reasoning.encrypted_content"])` on the - run. Plaintext reasoning cannot be re-ingested and is dropped inbound. -- The Responses API has no `tool` role — AG-UI `tool` messages become - `function_call_output` items linked by `call_id`. -- Unknown/unsupported message and event types never crash the translator — - they are dropped with a debug log. +Package tests do not automatically collect example tests. Run both suites +before opening a pull request that changes integration behavior or examples. -## Testing +After changing `openai-agents` or `openai`, always run the full suite. The drift +tests compare the event and item discriminators used by this integration with +the installed OpenAI Agents SDK so renamed or newly introduced wire types are caught before +release. -```bash -uv sync # installs dev group (pytest) -uv run pytest # run the full suite -``` +## Troubleshooting -The suite includes a **drift guard** (`tests/engine/test_types_drift.py`): -this package hardcodes the wire `type` strings it dispatches on (in -`engine/types.py`), and the guard asserts each one against the -`Literal[...]` annotations of the installed `openai-agents` / `openai` -packages. After bumping either dependency, run `uv run pytest` — if a wire -type was renamed or a new hosted tool-call item type was added, the guard -fails with an assertion diff naming the exact value to update in -`types.py`. Unknown types never crash at runtime (the translator -degrades gracefully and skips them); the guard exists so drift is caught in -CI instead of silently dropping events. +| Problem | Check | +|---|---| +| Authentication fails | Confirm `OPENAI_API_KEY` is set in the server process, not only in another terminal or frontend environment. | +| The client receives no stream | Return `StreamingResponse` with `EventEncoder.get_content_type()` and yield every encoded event from `translator.to_agui(...)`. | +| A frontend tool never appears | Include its definition in `RunAgentInput.tools`; frontend-owned tools are created from the request, not from the backend agent. | +| A backend approval cannot resume | Persist the OpenAI Agents SDK `RunState`, return the matching `call_id`, apply `state.approve()` or `state.reject()`, and resume with that state. The example's in-memory store is not production storage. | +| IDs do not match across events | Preserve the incoming AG-UI message and tool-call IDs; see [ID mapping and guarantees](#id-mapping-and-guarantees). | +| A run fails without useful client details | Set `run_error_message` for a safe client-facing message and inspect the original re-raised exception in server logs. | + +When reporting a bug, include the package versions, a minimal `RunAgentInput`, +the observed AG-UI event sequence, and a small reproducer. Remove API keys and +sensitive message content first. + +## Resources + +- [OpenAI Agents SDK documentation](https://openai.github.io/openai-agents-python/) +- [OpenAI Agents guide](https://developers.openai.com/api/docs/guides/agents) +- [AG-UI Protocol](https://github.com/ag-ui-protocol/ag-ui) +- [Integration source](https://github.com/ag-ui-protocol/ag-ui/tree/main/integrations/openai-agents) +- [Examples](examples/) +- [Dojo demos](https://dojo.ag-ui.com/openai-agents-python/feature/agentic_chat) +- [MIT License](LICENSE) From c3bc4fc921c08ac0c0673d2ed90aa5b307e63ecd Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 17:17:50 +0200 Subject: [PATCH 92/94] docs(openai-agents): polish ag_ui_openai_agents examples README.mdch --- .../openai-agents/python/examples/README.md | 121 ++++++++++++++++-- 1 file changed, 112 insertions(+), 9 deletions(-) diff --git a/integrations/openai-agents/python/examples/README.md b/integrations/openai-agents/python/examples/README.md index 85c6bf85d5..d6342bff7e 100644 --- a/integrations/openai-agents/python/examples/README.md +++ b/integrations/openai-agents/python/examples/README.md @@ -1,4 +1,4 @@ -# OpenAI Agents SDK examples +# OpenAI Agents (Python) examples Runnable demos for `ag_ui_openai_agents`, one mounted FastAPI app per agent. The aggregate server mounts each example application: @@ -14,13 +14,40 @@ Model provider is **native OpenAI** (`OPENAI_API_KEY`). These examples exercise the direct OpenAI path deliberately, since that is the reference setup for the library. -## Running the server +## Prerequisites + +- Python 3.10 or newer +- [`uv`](https://docs.astral.sh/uv/) +- An OpenAI API key + +## Setup and configuration ```bash -cd examples +cd integrations/openai-agents/python/examples uv sync --no-dev -cp .env.example .env # fill in OPENAI_API_KEY -uv run --no-dev python server.py +cp .env.example .env +``` + +Add your API key to `.env`. The example server recognizes: + +| Variable | Required | Purpose | +|---|---:|---| +| `OPENAI_API_KEY` | Yes | Authenticates OpenAI requests. | +| `OPENAI_DEFAULT_MODEL` | No | Overrides the model shared by the examples. | +| `OPENAI_AGENTS_DISABLE_TRACING` | No | Set to `true` to disable OpenAI Agents SDK tracing. | +| `HOST` | No | Bind address; defaults to `0.0.0.0`. | +| `PORT` | No | Server port; defaults to `8024`. | +| `RELOAD` | No | Set to a non-empty value to restart the server after local code changes. | + +These examples use the OpenAI provider directly. Provider selection is not an +AG-UI translator setting; applications that use another OpenAI Agents SDK +model provider configure that provider when constructing their agents and +runner. + +## Running the server + +```bash +uv run --env-file .env --no-dev python server.py ``` Server runs on **http://localhost:8024** (the port the AG-UI Dojo expects; @@ -28,6 +55,39 @@ override with `PORT`). Tracing follows the SDK's own `OPENAI_AGENTS_DISABLE_TRAC env var and default (`false` — tracing on); set it to `true` to disable tracing. +## Available endpoints + +| Endpoint | Demonstrates | +|---|---| +| `/ag_ui_docs_copilot/` | Documentation retrieval through backend tools | +| `/agentic_chat/` | Basic streamed conversation | +| `/backend_tool_rendering/` | Backend function tools and rendered results | +| `/human_in_the_loop/` | Frontend-owned tool execution | +| `/human_in_the_loop_approval/` | Approval before a backend tool runs | +| `/tool_based_generative_ui/` | UI rendered from frontend tool arguments | +| `/subagents/` | OpenAI Agents SDK agents-as-tools orchestration | +| `/custom_lifecycle_events/` | Custom events around a run | +| `/dynamic_system_prompt/` | Instructions derived from AG-UI context | +| `/health` | Server status and registered demo names | + +All demo endpoints accept AG-UI `RunAgentInput` requests and return an SSE +event stream. + +## Testing with Dojo + +Start the example server, then run the Dojo from the repository root in a +second terminal: + +```bash +cd apps/dojo +pnpm dev +``` + +Open `http://localhost:3000`, select **OpenAI Agents (Python)**, and choose a +feature. The Dojo uses `http://localhost:8024` by default. Set +`OPENAI_AGENTS_PYTHON_URL` in the Dojo environment if the backend uses another +address. + ## Automated tests The examples have an independent test suite. From this directory, install the @@ -60,10 +120,6 @@ curl -N -X POST http://localhost:8024/agentic_chat/ \ Swap the path to hit a different demo. `GET /health` on the aggregate server lists every registered agent. Demos map 1:1 onto the AG-UI Dojo feature pages. -> The stateful demos (`shared_state`, `agentic_generative_ui`, -> `predictive_state_updates`) are shelved together with the `AGUIContext` -> state bridge — see `.dev/shelved/` in the package root. - ## Agents ### `ag_ui_docs_copilot` @@ -187,3 +243,50 @@ itself. Each specialist invocation appears to the client as a normal stay internal to the SDK. **Try:** `"Write a short piece about the history of coffee."` + +### `custom_lifecycle_events` + +Adds application-defined lifecycle events around the normal translated OpenAI +Agents SDK stream. This demonstrates how an application can extend a run with +AG-UI `CUSTOM` events without changing the translator's built-in mappings. + +### `dynamic_system_prompt` + +Builds OpenAI Agents SDK instructions from AG-UI context supplied with the +request. This demonstrates application-owned prompt construction while the +translator continues to handle messages and streamed events. + +## How the examples work + +- `server.py` mounts every example FastAPI application under its demo name. +- Each module in `agents_examples/` owns its agent and endpoint behavior. +- Most demos use `OpenAIAgentsAgent` and + `add_openai_agents_fastapi_endpoint`. +- Demos that need custom lifecycle or pause/resume behavior use + `AGUITranslator` directly around `Runner.run_streamed(...)`. +- The package dependency points to the parent integration in editable mode, so + local integration changes are used without publishing a wheel. + +## Adding an example + +1. Add a module under `agents_examples/` containing the agent and FastAPI app. +2. Register its agent and app in `DEMOS` and `DEMO_APPS` in `server.py`. +3. Add the endpoint and behavior to this README. +4. Add focused tests under `tests/` and, when it is a Dojo feature, its Dojo + registration and end-to-end test. + +## Troubleshooting + +| Problem | Check | +|---|---| +| Server exits immediately | Set `OPENAI_API_KEY` in `examples/.env`. | +| Dojo cannot connect | Confirm the server is on port `8024`, or set `OPENAI_AGENTS_PYTHON_URL` for Dojo. | +| Endpoint returns 404 | Include the mounted demo path and its trailing slash, such as `/agentic_chat/`. | +| Changes are not picked up | Set `RELOAD=1` or restart the example server. | +| Example tests are not running | Run `uv run pytest` from the `examples` directory; the package suite does not collect them. | + +## References + +- [AG-UI Protocol documentation](https://docs.ag-ui.com/) +- [OpenAI Agents SDK documentation](https://openai.github.io/openai-agents-python/) +- [OpenAI Agents integration README](../README.md) From e7eef7a310e0b575d239d3f289383acc6b9e5440 Mon Sep 17 00:00:00 2001 From: Abdelrahman Abozied Date: Sun, 19 Jul 2026 17:37:01 +0200 Subject: [PATCH 93/94] docs(openai-agents): polish ag_ui_openai_agents dojo examples README.md --- .../(v2)/ag_ui_docs_copilot/README.mdx | 58 ++++++++----------- .../(v2)/custom_lifecycle_events/README.mdx | 49 ++++++---------- .../(v2)/dynamic_system_prompt/README.mdx | 39 +++++-------- .../human_in_the_loop_approval/README.mdx | 31 +++++++--- .../feature/(v2)/subagents/README.mdx | 35 +++++------ apps/dojo/src/files.json | 10 ++-- 6 files changed, 104 insertions(+), 118 deletions(-) diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx index a9de27bc48..ccac1e6327 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/README.mdx @@ -1,41 +1,31 @@ -# AG-UI Docs Copilot +# 📚 AG-UI Docs Copilot -This example is a small documentation assistant for the AG-UI integration with -the OpenAI Agents SDK. +## What This Demo Shows -One **Copilot** agent handles normal conversation directly, and for AG-UI or -OpenAI Agents SDK questions, it calls one of two tools, `read_ag_ui_protocol_docs` -and `read_ag_ui_openai_agents_docs`, to read a single section of the relevant -local README by heading. +A developer assistant that answers questions using the documentation packaged +with `ag-ui-protocol` and `ag-ui-openai-agents`. -## What it demonstrates +The agent receives the available README headings in its instructions. When a +question needs documentation, it calls one of two backend tools to load only +the relevant section from the installed Python package metadata. The tool call +and its progress are rendered in the chat. -```text -Copilot → read_ag_ui_protocol_docs / read_ag_ui_openai_agents_docs (local READMEs, one section at a time) - ↓ -RunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE -``` +No internet search or external retrieval service is used. -- Local documentation with no internet request or retrieval framework -- Table-of-contents instructions plus an on-demand section-read tool, instead - of loading a whole README into the prompt -- A direct, visible AG-UI translator endpoint -- Streaming lifecycle, text, and tool-call events to CopilotKit +## How to Interact -## Why the documentation is loaded by section - -The integration README is small enough for this focused demo, so its heading -list sits in the Copilot's instructions and each tool call reads back one -matching section. This keeps the example deterministic, cheap, and fast to -stream — an earlier version routed each question through a second -sub-agent, which cost extra model round trips before the first token showed -up. A production assistant with many or large documents should replace this -with its own retrieval system. - -## Try it - -- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and - SDK. +- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect an application + to AG-UI. - Ask for a minimal FastAPI streaming endpoint. -- Ask which translator options control lifecycle events, snapshots, state, and - errors. +- Ask how messages, tools, state, lifecycle events, or errors are translated. + +## Technical Details + +- `read_ag_ui_protocol_docs` reads the AG-UI Protocol Python README. +- `read_ag_ui_openai_agents_docs` reads the OpenAI Agents integration README. +- Each tool receives a heading and returns one matching Markdown section, + keeping the full documents out of the model prompt. +- The backend uses `AGUITranslator` directly around + `Runner.run_streamed()`, then encodes the resulting AG-UI events as SSE. +- For a larger documentation collection, replace the heading lookup with an + application-appropriate retrieval system. diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx index 97238c5278..ac7db1ef3d 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/custom_lifecycle_events/README.mdx @@ -2,40 +2,29 @@ ## What This Demo Shows -`AGUITranslator.to_agui()` takes an optional `end_custom_event` param — -an AG-UI `CustomEvent` instance (anything else raises `TypeError`), default -`None` (off). When set, one `CUSTOM` event is emitted right before -`RUN_FINISHED`. This demo uses it to report the run's real token usage: -`run_usage`, with `input_tokens`/`output_tokens` read off the SDK's own -`RunResultStreaming.context_wrapper.usage` after the stream finishes — plain -conversation otherwise, same agent as `agentic_chat`. +A custom AG-UI event can carry application-specific information alongside the +standard run events. This example emits a `run_usage` event containing the +actual input and output token counts, then displays those counts beneath the +completed assistant message. -Real usage is only known once the run is done — there's no such thing as a -true token count before the model has answered — so this only fires once, -at the end, not at both `RUN_STARTED` and `RUN_FINISHED` like a -demonstration bracketing pair would. +The usage is emitted at the end of the run because final token totals are only +available after the OpenAI Agents SDK stream has completed. ## How to Interact -- "Say hi in one sentence." -- "Tell me one interesting fact about AI." - -Each assistant reply has a compact usage label immediately after it, showing -the real input and output token counts for that run. +- Ask: "Say hi in one sentence." +- Ask: "Tell me one interesting fact about AI." +- After the response completes, check the input and output token counts shown + directly below it. ## Technical Details -- `agents_examples/custom_lifecycle_events.py` composes the translator - directly (`translator.to_openai`/`translator.to_agui`) instead of the - `OpenAIAgentsAgent` wrapper, because reading the run's real usage requires - the `RunResultStreaming` object the wrapper doesn't expose to a lifecycle - event factory -- `end_custom_event`'s value is a zero-argument closure over that `result`, - resolved by the translator right before it emits the event — by then the - stream is fully drained and `context_wrapper.usage` holds the run's actual - totals -- The translator itself is opaque to the event's contents — it only checks - `isinstance(..., CustomEvent)`; the `name`/`value` shape is entirely up to - the caller -- The page associates each `run_usage` event with the completed assistant - message and renders the usage directly after that message +- The backend uses `AGUITranslator` directly so it can access the + `RunResultStreaming` object and its final usage totals. +- It passes a `CustomEvent` named `run_usage` through the translator's + `end_custom_event` option. The event is emitted immediately before + `RUN_FINISHED`. +- The event value is resolved after the stream finishes, when + `context_wrapper.usage` contains the actual totals. +- The frontend subscribes to `run_usage`, associates it with the completed + assistant message, and renders a compact usage label. diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx index eb9974aeed..04ba4ca74f 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/dynamic_system_prompt/README.mdx @@ -2,33 +2,26 @@ ## What This Demo Shows -The frontend sends a language choice over the AG-UI **context** channel — a -plain `{description, value}` pair, no special class involved. The agent's -`instructions` is a callable (`Agent.instructions`, the OpenAI Agents SDK's -own dynamic-instructions feature) that reads the context list every turn and -bakes "always reply in X" into the system prompt fresh, before each model -call. +The frontend sends the selected reply language through the AG-UI context +channel. The backend reads that context on every run and uses the OpenAI Agents +SDK dynamic-instructions API to rebuild the agent's system prompt. -Context needs no `AGUIContext`/state machinery — it's read-only and -regenerated every run. The shared `OpenAIAgentsAgent` wrapper passes the -translated context into `Runner.run_streamed(..., context=...)`, so this demo -uses the same mounted FastAPI-app pattern as the other examples. +Changing the language affects the next response without restarting the server +or storing the selection in agent state. ## How to Interact -- Pick a language in the sidebar, then ask anything — the reply comes back - entirely in that language, regardless of what language you type in. -- Switch languages mid-conversation and ask again — the next reply follows - the new choice immediately, no restart needed. +- Select English, Arabic, or German in the sidebar, then ask a question. +- Change the language and send another message. The next response follows the + new selection immediately. ## Technical Details -- `useAgentContext({ description: "Reply language", value: language })` — - pushes the current pick onto `RunAgentInput.context` with every message -- `dynamic_instructions(ctx, agent)` in - `examples/agents_examples/dynamic_system_prompt.py` reads - `ctx.context` (the raw `list[Context]` sent by the client) and returns a - fresh instructions string -- The demo app is mounted by `examples/server.py` at - `/dynamic_system_prompt`; the aggregate OpenAI Agents server runs on port - 8024 +- `useAgentContext` adds `{ description: "Reply language", value: language }` + to `RunAgentInput.context` on every request. +- `OpenAIAgentsAgent` forwards the translated context to + `Runner.run_streamed(..., context=...)`. +- The agent's `instructions` callable reads `RunContextWrapper.context` and + returns a new system prompt for that run. +- Context is read-only request information in this example; no shared-state + bridge is required. diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/README.mdx index b4b7d43db9..ee9c472376 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/README.mdx @@ -3,12 +3,29 @@ title: Human in the Loop Approval description: Pause a backend tool call until a person approves or rejects it. --- -## Human in the Loop Approval +# 🤝 Human in the Loop Approval -This example uses a backend-owned `issue_refund` tool with the OpenAI Agents -SDK's `needs_approval` option. When the agent asks to issue a refund, the run -pauses and the page shows an approval card. +## What This Demo Shows -Choose **Approve** to execute the refund or **Reject** to resume the agent -without executing it. The server keeps the paused SDK state for the thread and -uses the decision from the next request to continue that same run. +A backend-owned refund tool pauses before execution and asks the user for an +explicit decision. Approving runs the tool; rejecting resumes the agent +without issuing the refund. + +## How to Interact + +- Ask: "Refund order ORD-1001." +- Select **Approve** to execute the refund, or **Reject** to continue without + executing it. +- Ask about the order afterward to see how the agent handled the decision. + +## Technical Details + +- `issue_refund` uses the OpenAI Agents SDK's `needs_approval=True` option. +- After the SDK reports an interruption, the backend saves `result.to_state()` + under the AG-UI `thread_id` and emits an `approval_request` custom event. +- The frontend sends the selected decision and matching tool `call_id` in the + next request's `forwarded_props`. +- The backend applies `state.approve()` or `state.reject()` and resumes the + saved OpenAI Agents SDK run. +- This demo keeps paused runs in process memory. Production applications need + durable, concurrency-safe storage shared by all server instances. diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx index 79bea8bda3..997e70953e 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/README.mdx @@ -2,29 +2,26 @@ ## What This Demo Shows -A supervisor agent stays in charge of the conversation and calls specialist -agents as tools (`Agent.as_tool()`), possibly several in one turn, then -synthesizes their outputs itself — a call-and-return delegation, not a -handoff (control never transfers away from the supervisor). Same shape as -CopilotKit's own LangGraph "subagents" showcase demo (a supervisor calling -child agents as tools). +A supervisor agent delegates a writing task to three specialist agents: +researcher, writer, and critic. The supervisor remains responsible for the +conversation, calls the specialists as tools, and produces the final response. -A sidebar tracks the team live: role chips light up while a specialist is -working, and a delegation log lists each call with its status and a -preview of the input/output. +The sidebar shows which specialist is working and records every delegation in +order. Simple questions can be answered directly without calling the team. ## How to Interact -- "Write a short piece about the history of coffee" (calls research, - writer, and critic specialists in sequence, then produces a final draft) -- "What is 2 + 2?" (answered directly, no team involved) +- Ask: "Write a short piece about the history of coffee." The supervisor + calls the researcher, writer, and critic before returning the final draft. +- Ask: "What is 2 + 2?" The supervisor answers directly without delegating. ## Technical Details -- Each specialist invocation is an ordinary `TOOL_CALL_START/ARGS/END` + - `TOOL_CALL_RESULT` sequence -- The nested agents' own model turns stay internal to the SDK and are not - streamed as separate messages — the client sees one coherent transcript -- The sidebar is built from three `useRenderTool` renderers (one per - specialist tool name), each reporting its status into shared page state - via a small tracker component +- The OpenAI Agents SDK exposes each specialist through `Agent.as_tool()`. + This is call-and-return delegation; it is not a handoff of the conversation. +- Each delegation becomes an AG-UI tool-call sequence followed by a + `TOOL_CALL_RESULT` event. +- A `useRenderTool` renderer for each specialist updates the role indicators + and delegation log. +- The specialists' internal model turns remain inside the OpenAI Agents SDK; + the client receives one supervisor-owned conversation. diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 2ad0608a91..21dd825a8a 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -4954,7 +4954,7 @@ }, { "name": "README.mdx", - "content": "# AG-UI Docs Copilot\n\nThis example is a small documentation assistant for the AG-UI integration with\nthe OpenAI Agents SDK.\n\nOne **Copilot** agent handles normal conversation directly, and for AG-UI or\nOpenAI Agents SDK questions, it calls one of two tools, `read_ag_ui_protocol_docs`\nand `read_ag_ui_openai_agents_docs`, to read a single section of the relevant\nlocal README by heading.\n\n## What it demonstrates\n\n```text\nCopilot → read_ag_ui_protocol_docs / read_ag_ui_openai_agents_docs (local READMEs, one section at a time)\n ↓\nRunAgentInput → to_openai() → Runner.run_streamed() → to_agui() → SSE\n```\n\n- Local documentation with no internet request or retrieval framework\n- Table-of-contents instructions plus an on-demand section-read tool, instead\n of loading a whole README into the prompt\n- A direct, visible AG-UI translator endpoint\n- Streaming lifecycle, text, and tool-call events to CopilotKit\n\n## Why the documentation is loaded by section\n\nThe integration README is small enough for this focused demo, so its heading\nlist sits in the Copilot's instructions and each tool call reads back one\nmatching section. This keeps the example deterministic, cheap, and fast to\nstream — an earlier version routed each question through a second\nsub-agent, which cost extra model round trips before the first token showed\nup. A production assistant with many or large documents should replace this\nwith its own retrieval system.\n\n## Try it\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect the frontend and\n SDK.\n- Ask for a minimal FastAPI streaming endpoint.\n- Ask which translator options control lifecycle events, snapshots, state, and\n errors.\n", + "content": "# 📚 AG-UI Docs Copilot\n\n## What This Demo Shows\n\nA developer assistant that answers questions using the documentation packaged\nwith `ag-ui-protocol` and `ag-ui-openai-agents`.\n\nThe agent receives the available README headings in its instructions. When a\nquestion needs documentation, it calls one of two backend tools to load only\nthe relevant section from the installed Python package metadata. The tool call\nand its progress are rendered in the chat.\n\nNo internet search or external retrieval service is used.\n\n## How to Interact\n\n- Ask how `AGUITranslator.to_openai()` and `to_agui()` connect an application\n to AG-UI.\n- Ask for a minimal FastAPI streaming endpoint.\n- Ask how messages, tools, state, lifecycle events, or errors are translated.\n\n## Technical Details\n\n- `read_ag_ui_protocol_docs` reads the AG-UI Protocol Python README.\n- `read_ag_ui_openai_agents_docs` reads the OpenAI Agents integration README.\n- Each tool receives a heading and returns one matching Markdown section,\n keeping the full documents out of the model prompt.\n- The backend uses `AGUITranslator` directly around\n `Runner.run_streamed()`, then encodes the resulting AG-UI events as SSE.\n- For a larger documentation collection, replace the heading lookup with an\n application-appropriate retrieval system.\n", "language": "markdown", "type": "file" }, @@ -5040,7 +5040,7 @@ }, { "name": "README.mdx", - "content": "---\ntitle: Human in the Loop Approval\ndescription: Pause a backend tool call until a person approves or rejects it.\n---\n\n## Human in the Loop Approval\n\nThis example uses a backend-owned `issue_refund` tool with the OpenAI Agents\nSDK's `needs_approval` option. When the agent asks to issue a refund, the run\npauses and the page shows an approval card.\n\nChoose **Approve** to execute the refund or **Reject** to resume the agent\nwithout executing it. The server keeps the paused SDK state for the thread and\nuses the decision from the next request to continue that same run.\n", + "content": "---\ntitle: Human in the Loop Approval\ndescription: Pause a backend tool call until a person approves or rejects it.\n---\n\n# 🤝 Human in the Loop Approval\n\n## What This Demo Shows\n\nA backend-owned refund tool pauses before execution and asks the user for an\nexplicit decision. Approving runs the tool; rejecting resumes the agent\nwithout issuing the refund.\n\n## How to Interact\n\n- Ask: \"Refund order ORD-1001.\"\n- Select **Approve** to execute the refund, or **Reject** to continue without\n executing it.\n- Ask about the order afterward to see how the agent handled the decision.\n\n## Technical Details\n\n- `issue_refund` uses the OpenAI Agents SDK's `needs_approval=True` option.\n- After the SDK reports an interruption, the backend saves `result.to_state()`\n under the AG-UI `thread_id` and emits an `approval_request` custom event.\n- The frontend sends the selected decision and matching tool `call_id` in the\n next request's `forwarded_props`.\n- The backend applies `state.approve()` or `state.reject()` and resumes the\n saved OpenAI Agents SDK run.\n- This demo keeps paused runs in process memory. Production applications need\n durable, concurrency-safe storage shared by all server instances.\n", "language": "markdown", "type": "file" }, @@ -5086,7 +5086,7 @@ }, { "name": "README.mdx", - "content": "# 🧭 Subagents\n\n## What This Demo Shows\n\nA supervisor agent stays in charge of the conversation and calls specialist\nagents as tools (`Agent.as_tool()`), possibly several in one turn, then\nsynthesizes their outputs itself — a call-and-return delegation, not a\nhandoff (control never transfers away from the supervisor). Same shape as\nCopilotKit's own LangGraph \"subagents\" showcase demo (a supervisor calling\nchild agents as tools).\n\nA sidebar tracks the team live: role chips light up while a specialist is\nworking, and a delegation log lists each call with its status and a\npreview of the input/output.\n\n## How to Interact\n\n- \"Write a short piece about the history of coffee\" (calls research,\n writer, and critic specialists in sequence, then produces a final draft)\n- \"What is 2 + 2?\" (answered directly, no team involved)\n\n## Technical Details\n\n- Each specialist invocation is an ordinary `TOOL_CALL_START/ARGS/END` +\n `TOOL_CALL_RESULT` sequence\n- The nested agents' own model turns stay internal to the SDK and are not\n streamed as separate messages — the client sees one coherent transcript\n- The sidebar is built from three `useRenderTool` renderers (one per\n specialist tool name), each reporting its status into shared page state\n via a small tracker component\n", + "content": "# 🧭 Subagents\n\n## What This Demo Shows\n\nA supervisor agent delegates a writing task to three specialist agents:\nresearcher, writer, and critic. The supervisor remains responsible for the\nconversation, calls the specialists as tools, and produces the final response.\n\nThe sidebar shows which specialist is working and records every delegation in\norder. Simple questions can be answered directly without calling the team.\n\n## How to Interact\n\n- Ask: \"Write a short piece about the history of coffee.\" The supervisor\n calls the researcher, writer, and critic before returning the final draft.\n- Ask: \"What is 2 + 2?\" The supervisor answers directly without delegating.\n\n## Technical Details\n\n- The OpenAI Agents SDK exposes each specialist through `Agent.as_tool()`.\n This is call-and-return delegation; it is not a handoff of the conversation.\n- Each delegation becomes an AG-UI tool-call sequence followed by a\n `TOOL_CALL_RESULT` event.\n- A `useRenderTool` renderer for each specialist updates the role indicators\n and delegation log.\n- The specialists' internal model turns remain inside the OpenAI Agents SDK;\n the client receives one supervisor-owned conversation.\n", "language": "markdown", "type": "file" }, @@ -5106,7 +5106,7 @@ }, { "name": "README.mdx", - "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\n`AGUITranslator.to_agui()` takes an optional `end_custom_event` param —\nan AG-UI `CustomEvent` instance (anything else raises `TypeError`), default\n`None` (off). When set, one `CUSTOM` event is emitted right before\n`RUN_FINISHED`. This demo uses it to report the run's real token usage:\n`run_usage`, with `input_tokens`/`output_tokens` read off the SDK's own\n`RunResultStreaming.context_wrapper.usage` after the stream finishes — plain\nconversation otherwise, same agent as `agentic_chat`.\n\nReal usage is only known once the run is done — there's no such thing as a\ntrue token count before the model has answered — so this only fires once,\nat the end, not at both `RUN_STARTED` and `RUN_FINISHED` like a\ndemonstration bracketing pair would.\n\n## How to Interact\n\n- \"Say hi in one sentence.\"\n- \"Tell me one interesting fact about AI.\"\n\nEach assistant reply has a compact usage label immediately after it, showing\nthe real input and output token counts for that run.\n\n## Technical Details\n\n- `agents_examples/custom_lifecycle_events.py` composes the translator\n directly (`translator.to_openai`/`translator.to_agui`) instead of the\n `OpenAIAgentsAgent` wrapper, because reading the run's real usage requires\n the `RunResultStreaming` object the wrapper doesn't expose to a lifecycle\n event factory\n- `end_custom_event`'s value is a zero-argument closure over that `result`,\n resolved by the translator right before it emits the event — by then the\n stream is fully drained and `context_wrapper.usage` holds the run's actual\n totals\n- The translator itself is opaque to the event's contents — it only checks\n `isinstance(..., CustomEvent)`; the `name`/`value` shape is entirely up to\n the caller\n- The page associates each `run_usage` event with the completed assistant\n message and renders the usage directly after that message\n", + "content": "# 🪝 Custom Lifecycle Events\n\n## What This Demo Shows\n\nA custom AG-UI event can carry application-specific information alongside the\nstandard run events. This example emits a `run_usage` event containing the\nactual input and output token counts, then displays those counts beneath the\ncompleted assistant message.\n\nThe usage is emitted at the end of the run because final token totals are only\navailable after the OpenAI Agents SDK stream has completed.\n\n## How to Interact\n\n- Ask: \"Say hi in one sentence.\"\n- Ask: \"Tell me one interesting fact about AI.\"\n- After the response completes, check the input and output token counts shown\n directly below it.\n\n## Technical Details\n\n- The backend uses `AGUITranslator` directly so it can access the\n `RunResultStreaming` object and its final usage totals.\n- It passes a `CustomEvent` named `run_usage` through the translator's\n `end_custom_event` option. The event is emitted immediately before\n `RUN_FINISHED`.\n- The event value is resolved after the stream finishes, when\n `context_wrapper.usage` contains the actual totals.\n- The frontend subscribes to `run_usage`, associates it with the completed\n assistant message, and renders a compact usage label.\n", "language": "markdown", "type": "file" }, @@ -5126,7 +5126,7 @@ }, { "name": "README.mdx", - "content": "# 🌐 Dynamic System Prompt\n\n## What This Demo Shows\n\nThe frontend sends a language choice over the AG-UI **context** channel — a\nplain `{description, value}` pair, no special class involved. The agent's\n`instructions` is a callable (`Agent.instructions`, the OpenAI Agents SDK's\nown dynamic-instructions feature) that reads the context list every turn and\nbakes \"always reply in X\" into the system prompt fresh, before each model\ncall.\n\nContext needs no `AGUIContext`/state machinery — it's read-only and\nregenerated every run. The shared `OpenAIAgentsAgent` wrapper passes the\ntranslated context into `Runner.run_streamed(..., context=...)`, so this demo\nuses the same mounted FastAPI-app pattern as the other examples.\n\n## How to Interact\n\n- Pick a language in the sidebar, then ask anything — the reply comes back\n entirely in that language, regardless of what language you type in.\n- Switch languages mid-conversation and ask again — the next reply follows\n the new choice immediately, no restart needed.\n\n## Technical Details\n\n- `useAgentContext({ description: \"Reply language\", value: language })` —\n pushes the current pick onto `RunAgentInput.context` with every message\n- `dynamic_instructions(ctx, agent)` in\n `examples/agents_examples/dynamic_system_prompt.py` reads\n `ctx.context` (the raw `list[Context]` sent by the client) and returns a\n fresh instructions string\n- The demo app is mounted by `examples/server.py` at\n `/dynamic_system_prompt`; the aggregate OpenAI Agents server runs on port\n 8024\n", + "content": "# 🌐 Dynamic System Prompt\n\n## What This Demo Shows\n\nThe frontend sends the selected reply language through the AG-UI context\nchannel. The backend reads that context on every run and uses the OpenAI Agents\nSDK dynamic-instructions API to rebuild the agent's system prompt.\n\nChanging the language affects the next response without restarting the server\nor storing the selection in agent state.\n\n## How to Interact\n\n- Select English, Arabic, or German in the sidebar, then ask a question.\n- Change the language and send another message. The next response follows the\n new selection immediately.\n\n## Technical Details\n\n- `useAgentContext` adds `{ description: \"Reply language\", value: language }`\n to `RunAgentInput.context` on every request.\n- `OpenAIAgentsAgent` forwards the translated context to\n `Runner.run_streamed(..., context=...)`.\n- The agent's `instructions` callable reads `RunContextWrapper.context` and\n returns a new system prompt for that run.\n- Context is read-only request information in this example; no shared-state\n bridge is required.\n", "language": "markdown", "type": "file" }, From 5b7e3c6865f1135c93f8a654b4b0ac8645f14cbf Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Tue, 28 Jul 2026 16:00:46 +0200 Subject: [PATCH 94/94] CR fixes for OpenAI Agents SDK (Python) integration (stacked on #2210) (#1) * fix(openai-agents): CR round-1 fixes (correctness, robustness, tests) Engine: - translate(): read run_input.resume via getattr (defensive, matches parent_run_id and the documented contract) - outbound: reuse the real item id on lazy text-open instead of a generated one (honors the id-reuse invariant; avoids snapshot/stream id divergence) - outbound: buffer function-call args that stream before output_item.added instead of opening under a throwaway id, so the run-item commit no longer emits a duplicate TOOL_CALL_START/ARGS/END; finalize flushes orphans - outbound: surface REASONING_ENCRYPTED_VALUE on the run-item skip path so encrypted reasoning replay data is not dropped - helpers.to_string(): serialize pydantic models via model_dump() - helpers.__all__: add new_reasoning_id - translate_content(): drop empty/None content so empty turns are omitted Wrapper / endpoint: - OpenAIAgentsAgent: add context= injection point (falls back to AG-UI context); dedup client tools against each other - endpoint: log re-raised run errors instead of leaking an ASGI traceback; unique per-route name/operation_id so multiple agents can mount on one app Examples/dojo: - example DEFAULT_MODEL fallback -> gpt-5.5 - ApprovalCard dark-mode variants; consumerAgentId on docs_copilot & subagents - subagents e2e: scope delegation-log assertions to the log container (were matching the always-rendered team chips) Tests: covering tests for every behavioral change; snapshot test now guards against duplicate id emission. * fix(openai-agents): CR round-2 fixes (placeholder-id correlation, endpoint, ids) Confirmation round found a cluster of bugs the round-1 fixes exposed for non-native (placeholder / FAKE_RESPONSES_ID) backends, plus a few pre-existing: Outbound translator: - _is_real_id no longer misclassifies internal '__idx_' placeholder window keys as wire ids (add _is_placeholder_key guard): lazy text-open and the arg-buffer paths no longer leak placeholder keys onto the wire as message/tool_call ids - _window_key active-set now includes _pending_tool_args, so a placeholder window holding only buffered args is recognized and reused instead of split in two - run-item commit clears the correlated arg buffer (by item id, else oldest FIFO) so finalize no longer re-emits a phantom empty-name tool call - finalize drops orphaned (never-committed) arg buffers instead of emitting them - REASONING_ENCRYPTED_VALUE dedup keyed on the stable phase id (raw + skip paths) - reasoning delta-first reuses the real wire id (mirrors output_item.added) Wrapper / endpoint: - endpoint surfaces a terminal RUN_ERROR when setup fails before to_agui (was an empty 200); slug uniqueness guard so paths normalizing to one slug don't collide - translator honors an explicit empty run_error_message (only None -> str(exc)) - helpers.to_string logs a model_dump() failure instead of swallowing silently Examples/dojo: - run-dojo-everything.js OPENAI_CHAT_MODEL_ID default and commented model refs in agents.ts -> gpt-5.5 Tests: placeholder-backend coverage (arg flush/no-split, commit no-phantom, clean text id, reasoning id reuse), endpoint setup-error + slug-collision, empty run_error_message, and the text 'skip' branch now actually exercised in snapshot. * fix(openai-agents): CR round-3 fixes (SDK-floor compat, tool errors, buffer safety) Second confirmation round surfaced two regressions from the round-2 buffer fixes plus several pre-existing bugs: Outbound translator: - translate_tool_call_output_item reads call_id defensively (getattr + raw fallback): ToolCallOutputItem.call_id does not exist on the declared openai-agents>=0.8.4 floor, so a direct access turned every tool result into a RUN_ERROR on the minimum supported SDK - remove the blind FIFO buffer-drop in the tool-call 'new' branch: it could evict a *different* still-streaming call's buffered args; an uncorrelated placeholder buffer is harmlessly dropped by finalize instead Inbound translator: - translate_tool_message surfaces ToolMessage.error when content is empty (was silently sending a blank tool result the model can't recover from) - translate_document_content synthesizes a filename for base64 file_data (the Responses API requires one; mirrors _binary_as_file) Endpoint: - fallback now keys on RUN_STARTED: emits a well-formed RUN_STARTED+RUN_ERROR only on a true setup failure (run never started); stays silent (logs only) once the run started, so an explicit emit_run_error=False is honored and the terminal is never a lone RUN_ERROR without RUN_STARTED Dojo: OPENAI_AGENTS_PYTHON_URL wired into the dojo env blocks for parity. Tests: inbound message-type coverage (system/developer/assistant/reasoning/ tool-with-error/activity/context), defensive tool-output call_id, document filename, and caplog level consistency. * fix(openai-agents): CR round-4 fixes (filename synthesis, context/reasoning edges) Third confirmation round confirmed the round-3 fixes correct and surfaced a few small pre-existing edges plus one polish item on the round-3 endpoint change: - _binary_as_file synthesizes a filename for base64 file_data (mirrors translate_document_content); a filename-less binary file part was otherwise rejected by the Responses API. Comment on the document path corrected. - agent.py normalizes an empty ambient context to None (SDK default) instead of passing [], so agents branching on 'ctx.context is None' behave correctly. - endpoint setup-failure fallback RUN_STARTED now carries parent_run_id, matching the normal to_agui lifecycle path. - translate_context drops items missing either description or value (no more half-empty 'label: ' / ': value' lines). - translate_reasoning_message omits the summary entry entirely when content is empty (an empty summary_text is rejected by some backends). Tests updated/added for each; the prior test asserting the old (buggy) no-filename binary behavior now asserts the synthesized filename. * fix(openai-agents): CR round-5 fixes (SkipValidation on messages, refusal, mime) Fourth confirmation round confirmed the round-4 fixes correct; three reviewers independently traced two real bugs to one root cause -- TranslatedInput.messages being pydantic-validated against the Responses input union: - Audio input crashed to_openai(): input_audio is a valid Chat-Completions shape but not a Responses input-item member, so constructing TranslatedInput raised ValidationError and failed the WHOLE request (not just the audio part). - A reasoning item's summary (SDK-typed Iterable) was materialized as a one-shot pydantic ValidatorIterator that empties after a single read -- silent reasoning- replay data loss if messages is read more than once. Fix: mark TranslatedInput.messages SkipValidation (mirrors tools, same forward-ref rationale). The translator already guarantees well-formed shapes (e.g. images always get detail) and the SDK validates what it consumes at run time, so the removed strict validation was a redundant safety net that rejected otherwise- valid multimodal input. Audio now translates and reasoning summary stays a list. Also: - translate_message_output_item concatenates text + refusal into the snapshot (extract_text ignores refusals, so a fallback dropped a streamed refusal). - _audio_format_from_mime strips mime parameters ('audio/wav; codecs=1') before matching, so a parameterized-but-valid type isn't silently dropped. - endpoint comment clarifies the setup-failure terminal is emitted even when emit_run_error=False (a silent empty 200 is worse than a terminal error). Tests: audio-survives-to_openai, reasoning-summary-stable-list, text+refusal snapshot, mime-parameter parsing; the obsolete reject-image-without-detail test is replaced with a SkipValidation behavior test. --- .../subagentsPage.spec.ts | 8 +- apps/dojo/scripts/run-dojo-everything.js | 4 +- apps/dojo/src/agents.ts | 4 +- .../feature/(v2)/ag_ui_docs_copilot/page.tsx | 1 + .../(v2)/human_in_the_loop_approval/page.tsx | 8 +- .../feature/(v2)/subagents/page.tsx | 1 + .../examples/agents_examples/constants.py | 2 +- .../python/src/ag_ui_openai_agents/agent.py | 29 +- .../src/ag_ui_openai_agents/endpoint.py | 64 +++- .../engine/agui_to_openai.py | 53 +++- .../src/ag_ui_openai_agents/engine/helpers.py | 12 + .../engine/openai_to_agui.py | 122 ++++++-- .../src/ag_ui_openai_agents/engine/types.py | 18 +- .../src/ag_ui_openai_agents/translator.py | 4 +- .../engine/test_agui_to_openai_multimodal.py | 285 ++++++++++++++++-- .../tests/engine/test_openai_to_agui.py | 255 +++++++++++++++- .../engine/test_openai_to_agui_snapshot.py | 18 +- .../openai-agents/python/tests/test_agent.py | 60 +++- .../python/tests/test_endpoint.py | 104 +++++++ .../python/tests/test_translator.py | 25 ++ 20 files changed, 989 insertions(+), 88 deletions(-) diff --git a/apps/dojo/e2e/tests/openaiAgentsPythonTests/subagentsPage.spec.ts b/apps/dojo/e2e/tests/openaiAgentsPythonTests/subagentsPage.spec.ts index c8a44ab7f0..41f31984e1 100644 --- a/apps/dojo/e2e/tests/openaiAgentsPythonTests/subagentsPage.spec.ts +++ b/apps/dojo/e2e/tests/openaiAgentsPythonTests/subagentsPage.spec.ts @@ -12,7 +12,13 @@ test("[OpenAI Agents Python] Subagents records all specialist delegations", asyn "Write a short article about the history of AI", ); - const log = page.getByText("Delegation log").locator(".."); + // Scope to the delegation-log entries container (the div immediately after + // the "Delegation log" heading), NOT the whole sidebar — the sidebar also + // statically renders the "Supervisor's team" chips labelled Researcher/Writer/ + // Critic, which would make these assertions pass even if no delegation ran. + const log = page + .getByText("Delegation log") + .locator("xpath=following-sibling::div[1]"); await expect(log.getByText("Researcher").last()).toBeVisible(); await expect(log.getByText("Writer").last()).toBeVisible(); await expect(log.getByText("Critic").last()).toBeVisible(); diff --git a/apps/dojo/scripts/run-dojo-everything.js b/apps/dojo/scripts/run-dojo-everything.js index b76cdb09f2..6efd8e6cc5 100755 --- a/apps/dojo/scripts/run-dojo-everything.js +++ b/apps/dojo/scripts/run-dojo-everything.js @@ -322,6 +322,7 @@ const ALL_SERVICES = { CLAUDE_AGENT_SDK_PYTHON_URL: "http://localhost:8019", CLAUDE_AGENT_SDK_TYPESCRIPT_URL: "http://localhost:8020", LANGROID_URL: "http://localhost:8021", + OPENAI_AGENTS_PYTHON_URL: "http://localhost:8024", NEXT_PUBLIC_CUSTOM_DOMAIN_TITLE: "cpkdojo.local___CopilotKit Feature Viewer", }, @@ -358,6 +359,7 @@ const ALL_SERVICES = { CLAUDE_AGENT_SDK_PYTHON_URL: "http://localhost:8019", CLAUDE_AGENT_SDK_TYPESCRIPT_URL: "http://localhost:8020", LANGROID_URL: "http://localhost:8021", + OPENAI_AGENTS_PYTHON_URL: "http://localhost:8024", NEXT_PUBLIC_CUSTOM_DOMAIN_TITLE: "cpkdojo.local___CopilotKit Feature Viewer", }, @@ -405,7 +407,7 @@ async function main() { OPENAI_BASE_URL: process.env.OPENAI_BASE_URL || "http://localhost:5555/v1", OPENAI_API_BASE: process.env.OPENAI_API_BASE || "http://localhost:5555/v1", OPENAI_API_KEY: process.env.OPENAI_API_KEY || "sk-mock", - OPENAI_CHAT_MODEL_ID: process.env.OPENAI_CHAT_MODEL_ID || "gpt-4o", + OPENAI_CHAT_MODEL_ID: process.env.OPENAI_CHAT_MODEL_ID || "gpt-5.5", }; // LLMock: inject GOOGLE_GEMINI_BASE_URL so ADK middleware agents (which keep diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 8dbebc6ac9..1fe62eb6bc 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -219,7 +219,7 @@ export const agentsIntegrations = { // Disabled until we can support Vercel AI SDK v5 // "vercel-ai-sdk": async () => ({ - // agentic_chat: new VercelAISDKAgent({ model: openai("gpt-4o") }), + // agentic_chat: new VercelAISDKAgent({ model: openai("gpt-5.5") }), // }), langgraph: async () => ({ @@ -348,7 +348,7 @@ export const agentsIntegrations = { // const agent = new LangChainAgent({ // chainFn: async ({ messages, tools, threadId }) => { // const { ChatOpenAI } = await import("@langchain/openai"); - // const chatOpenAI = new ChatOpenAI({ model: "gpt-4o" }); + // const chatOpenAI = new ChatOpenAI({ model: "gpt-5.5" }); // const model = chatOpenAI.bindTools(tools, { // strict: true, // }); diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx index e67949eec3..397f949f44 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/ag_ui_docs_copilot/page.tsx @@ -75,6 +75,7 @@ const DocsChat = () => { }, ], available: "always", + consumerAgentId: "ag_ui_docs_copilot", }); return ( diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx index 9df83ca60e..3f677f7625 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop_approval/page.tsx @@ -131,14 +131,14 @@ function ApprovalCard({ return (

Approval needed

-

+

The agent wants to call{" "} {pending.toolName} with:

-
+      
         {JSON.stringify(args, null, 2)}
       
@@ -146,7 +146,7 @@ function ApprovalCard({ type="button" data-testid="reject-button" onClick={onReject} - className="px-6 py-2 rounded-lg font-semibold bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-300" + className="px-6 py-2 rounded-lg font-semibold bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-100 dark:border-gray-600" > Reject diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx index 2b4329e04b..6d35af32da 100644 --- a/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/subagents/page.tsx @@ -79,6 +79,7 @@ const SubagentsView = () => { }, ], available: "always", + consumerAgentId: "subagents", }); useRenderTool({ diff --git a/integrations/openai-agents/python/examples/agents_examples/constants.py b/integrations/openai-agents/python/examples/agents_examples/constants.py index 5d25430286..80b615ad32 100644 --- a/integrations/openai-agents/python/examples/agents_examples/constants.py +++ b/integrations/openai-agents/python/examples/agents_examples/constants.py @@ -10,4 +10,4 @@ import os -DEFAULT_MODEL = os.getenv("OPENAI_DEFAULT_MODEL", "gpt-4.1-mini") +DEFAULT_MODEL = os.getenv("OPENAI_DEFAULT_MODEL", "gpt-5.5") diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py index 6da0dd6407..f97b748431 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/agent.py @@ -10,6 +10,10 @@ logger = logging.getLogger(__name__) +# Sentinel: distinguishes "caller passed context=None" (a real SDK context of +# None) from "caller said nothing" (fall back to the AG-UI context list). +_UNSET_CONTEXT: Any = object() + class OpenAIAgentsAgent: """Wrap an OpenAI Agents SDK Agent so it speaks the AG-UI protocol. @@ -39,6 +43,7 @@ def __init__( description: str = "", translator: AGUITranslator | None = None, run_config: RunConfig | None = None, + context: Any = _UNSET_CONTEXT, start_custom_event: CustomEvent | None = None, initial_state: Any = None, final_state: Any = None, @@ -57,6 +62,11 @@ def __init__( through engine subclasses. run_config: Run-wide OpenAI Agents SDK configuration. None uses the SDK defaults. + context: The SDK run context (``TContext``) handed to tools, hooks, + and dynamic instructions via ``RunContextWrapper.context``. When + omitted, the AG-UI request's ambient ``context`` list is passed + through instead. Provide this when the wrapped agent's tools + expect an application-specific context object. start_custom_event: CustomEvent emitted after RUN_STARTED. Its value may be static or a zero-argument sync/async factory. initial_state: Passed to AGUITranslator.to_agui unchanged. @@ -76,6 +86,7 @@ def __init__( self.description = description self._translator = translator or AGUITranslator() self._run_config = run_config + self._context = context self._start_custom_event = start_custom_event self._initial_state = initial_state self._final_state = final_state @@ -106,6 +117,7 @@ async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: if (name := getattr(tool, "name", None)) is not None } client_tools = [] + seen_client_names: set[str] = set() for tool in translated.tools: if tool.name in server_tool_names: logger.warning( @@ -113,6 +125,13 @@ async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: tool.name, ) continue + if tool.name in seen_client_names: + logger.warning( + "Ignoring duplicate client tool %r; the SDK cannot resolve two tools with one name", + tool.name, + ) + continue + seen_client_names.add(tool.name) client_tools.append(tool) if client_tools: @@ -120,11 +139,19 @@ async def run_streamed(self, input: RunAgentInput) -> AsyncIterator[BaseEvent]: tools=[*self.agent.tools, *client_tools] ) + # Fall back to the AG-UI ambient context; normalize an empty list to + # None so a context-less request matches the SDK's own default (None), + # not [] — agents that branch on `ctx.context is None` rely on this. + context = ( + self._context + if self._context is not _UNSET_CONTEXT + else (translated.context or None) + ) result = Runner.run_streamed( run_agent, input=translated.messages, run_config=self._run_config, - context=translated.context, + context=context, ) async for event in self._translator.to_agui( diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py index cbd0653b86..2cfe2963ed 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/endpoint.py @@ -1,12 +1,13 @@ """FastAPI endpoint helper for serving an OpenAI Agents SDK agent over AG-UI.""" import logging +import re from typing import AsyncIterator from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse -from ag_ui.core import RunAgentInput +from ag_ui.core import EventType, RunAgentInput, RunErrorEvent, RunStartedEvent from ag_ui.encoder import EventEncoder from .agent import OpenAIAgentsAgent @@ -29,8 +30,23 @@ def add_openai_agents_fastapi_endpoint( include_health: Whether to register a GET health check alongside the agent endpoint. Defaults to True. """ + # Unique per-route names/operation_ids so mounting several agents on one app + # does not collide (FastAPI derives both from the handler __name__ otherwise, + # producing duplicate operation ids and an ambiguous OpenAPI schema). + slug = re.sub(r"\W+", "_", path).strip("_") or "root" + # Guarantee uniqueness even if two mount paths normalize to the same slug + # (e.g. "/a/b" and "/a_b"), so route names / operation ids never collide. + used_slugs = getattr(app.state, "_openai_agents_slugs", None) + if used_slugs is None: + used_slugs = set() + app.state._openai_agents_slugs = used_slugs + base_slug, suffix = slug, 1 + while slug in used_slugs: + slug = f"{base_slug}_{suffix}" + suffix += 1 + used_slugs.add(slug) - @app.post(path) + @app.post(path, name=f"openai_agents_run_{slug}", operation_id=f"openai_agents_run_{slug}") async def openai_agents_endpoint( input_data: RunAgentInput, request: Request, @@ -46,8 +62,42 @@ async def openai_agents_endpoint( encoder = EventEncoder(accept=accept_header) async def event_generator() -> AsyncIterator[str]: - async for event in agent.run_streamed(input_data): - yield encoder.encode(event) + started = False + try: + async for event in agent.run_streamed(input_data): + if event.type == EventType.RUN_STARTED: + started = True + yield encoder.encode(event) + except Exception: + # If the run never emitted RUN_STARTED it failed in setup (input + # translation / clone / Runner.run_streamed) before to_agui ran, so + # no lifecycle reached the client — an empty 200 otherwise. Emit a + # minimal well-formed RUN_STARTED + RUN_ERROR — deliberately even + # when emit_run_error=False, since a silent empty 200 the client + # can't distinguish from success is worse than a terminal error at + # the transport boundary. If the run DID start, to_agui already + # handled the error per its emit_run_error config (possibly + # deliberately silent), so add nothing — just log. Either way, + # never sever the stream with an ASGI-level traceback. + if not started: + yield encoder.encode( + RunStartedEvent( + type=EventType.RUN_STARTED, + thread_id=input_data.thread_id, + run_id=input_data.run_id, + # Preserve parent linkage for nested/branched runs, + # matching the normal to_agui RUN_STARTED path. + parent_run_id=getattr(input_data, "parent_run_id", None), + ) + ) + yield encoder.encode( + RunErrorEvent(type=EventType.RUN_ERROR, message="Agent run failed") + ) + logger.exception( + "Agent run failed: agent=%s run_id=%s", + agent.name, + input_data.run_id, + ) return StreamingResponse( event_generator(), @@ -56,7 +106,11 @@ async def event_generator() -> AsyncIterator[str]: if include_health: - @app.get(path.rstrip("/") + "/health") + @app.get( + path.rstrip("/") + "/health", + name=f"openai_agents_health_{slug}", + operation_id=f"openai_agents_health_{slug}", + ) def health(): """Health check.""" return {"status": "ok", "agent": {"name": agent.name}} diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py index 71f67a1d03..f245f4f662 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/agui_to_openai.py @@ -63,7 +63,7 @@ def translate(self, run_input: RunAgentInput) -> TranslatedInput: the request identifiers, state, context, forwarded props, and resume entries in a ``TranslatedInput`` result. - Context and resume entries pass through unchanged.Context is not + Context and resume entries pass through unchanged. Context is not automatically added to model input; use ``translate_context()`` when the model should receive it. @@ -73,7 +73,9 @@ def translate(self, run_input: RunAgentInput) -> TranslatedInput: Returns: OpenAI Agents SDK inputs and preserved AG-UI request metadata. """ - resume = run_input.resume + # Read defensively — older RunAgentInput versions lack the field, the + # same reason parent_run_id below uses getattr (see types.py resume doc). + resume = getattr(run_input, "resume", None) return TranslatedInput( thread_id=run_input.thread_id, run_id=run_input.run_id, @@ -135,7 +137,7 @@ def translate_context( Example: prompt = "You are helpful." - ctx = translator.translate_context(bundle.context) + ctx = translator.translate_context(translated_input.context) if ctx: prompt = f"{prompt}\\n\\nContext:\\n{ctx}" @@ -148,7 +150,7 @@ def translate_context( lines = [ f"{item.description}: {item.value}" for item in items - if item.description or item.value + if item.description and item.value ] return "\n".join(lines) @@ -271,7 +273,13 @@ def translate_reasoning_message( "type": "reasoning", "id": message.id, "encrypted_content": message.encrypted_value, - "summary": [{"type": "summary_text", "text": message.content or ""}], + # Omit the summary entry entirely when there's no text — some backends + # reject a summary_text with an empty string. + "summary": ( + [{"type": "summary_text", "text": message.content}] + if message.content + else [] + ), } def translate_assistant_message( @@ -293,8 +301,9 @@ def translate_assistant_message( items: list[dict[str, Any]] = [] text = message.content or "" if text: - # Keep prior assistant text as an EasyInputMessageParam. - # Adding type="message" routes it to the SDK output-message converter. + # Keep prior assistant text as an EasyInputMessageParam (role+content). + # Do NOT add type="message" here: that reroutes it through the SDK's + # output-message converter instead of the EasyInputMessage path. items.append( { "role": "assistant", @@ -321,10 +330,13 @@ def translate_tool_message(self, message: ToolMessage) -> dict[str, Any]: Returns: {"type": "function_call_output", ...} """ + # Surface an error-only tool result: a ToolMessage may report failure in + # `error` with empty content (read defensively — the field is newer). + # Without this the model receives a blank output and cannot recover. return { "type": "function_call_output", "call_id": message.tool_call_id, - "output": message.content or "", + "output": message.content or getattr(message, "error", None) or "", } def translate_activity_message( @@ -410,7 +422,9 @@ def translate_content(self, content: Any) -> list[dict[str, Any]]: unsupported. """ if isinstance(content, str): - return [{"type": "input_text", "text": content}] + # Empty string carries nothing: return no parts so translate_user_message + # drops the turn instead of sending an empty content part the API rejects. + return [{"type": "input_text", "text": content}] if content else [] if isinstance(content, list): parts: list[dict[str, Any]] = [] for part in content: @@ -418,7 +432,8 @@ def translate_content(self, content: Any) -> list[dict[str, Any]]: if converted is not None: parts.append(converted) return parts - return [{"type": "input_text", "text": to_string(content)}] + text = to_string(content) + return [{"type": "input_text", "text": text}] if text else [] def translate_content_part(self, part: Any) -> dict[str, Any] | None: """Dispatch one content part to its per-type translator. @@ -552,8 +567,13 @@ def translate_document_content( if source_type == "url": return {"type": "input_file", "file_url": value} if source_type == "data": + # The Responses API requires a filename alongside base64 file_data; + # DocumentInputContent carries none, so synthesize one from the mime + # subtype (mirrors _binary_as_file). + subtype = (mime or "application/octet-stream").split("/")[-1] or "bin" return { "type": "input_file", + "filename": f"document.{subtype}", "file_data": f"data:{mime or 'application/octet-stream'};base64,{value}", } return None @@ -651,7 +671,9 @@ def _audio_format_from_mime(mime: str | None) -> str | None: """ if not mime: return "wav" - subtype = mime.split("/", 1)[-1].lower() + # Strip any parameters (e.g. "audio/wav; codecs=1") and whitespace before + # matching, so a parameterized-but-valid content type isn't dropped. + subtype = mime.split("/", 1)[-1].split(";", 1)[0].strip().lower() # Same audio, lots of names for it — fold the common ones together. if subtype in ("mpeg", "mpeg3", "mp3"): return "mp3" @@ -731,13 +753,14 @@ def _binary_as_file( if part.url: return {"type": "input_file", "file_url": part.url} if part.data: - payload = { + # The Responses API requires a filename alongside base64 file_data; + # synthesize one from the mime subtype when the part carries none. + subtype = mime.split("/")[-1] or "bin" + return { "type": "input_file", + "filename": part.filename or f"file.{subtype}", "file_data": f"data:{mime};base64,{part.data}", } - if part.filename: - payload["filename"] = part.filename - return payload return None diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py index 78930052df..77ec024962 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/helpers.py @@ -5,9 +5,12 @@ """ import json +import logging import uuid from typing import Any +logger = logging.getLogger(__name__) + # --------------------------------------------------------------------------- # ID generation @@ -82,6 +85,14 @@ def to_string(value: Any) -> str: return "" if isinstance(value, str): return value + # Pydantic models are a common tool return type; serialize their fields + # rather than falling through to an opaque quoted repr via default=str. + model_dump = getattr(value, "model_dump", None) + if callable(model_dump): + try: + value = model_dump() + except Exception as exc: # noqa: BLE001 — never let stringification raise + logger.debug("model_dump() failed during stringification: %s", exc) try: return json.dumps(value, default=str) except (TypeError, ValueError): @@ -90,6 +101,7 @@ def to_string(value: Any) -> str: __all__ = [ "new_message_id", + "new_reasoning_id", "new_tool_call_id", "new_tool_result_id", "read_attr", diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py index 1d65b1e327..ab2c4dc2d0 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/openai_to_agui.py @@ -82,6 +82,11 @@ class OpenAIToAGUITranslator: ``RUN_STARTED``, ``RUN_FINISHED``, ``RUN_ERROR``, and message/state snapshots. """ + # Prefix marking internal placeholder window keys minted by _window_key. + # These are correlation keys only and must never be emitted as AG-UI ids + # (unlike real wire item ids, which _is_real_id also reports truthy). + _PLACEHOLDER_PREFIX = "__idx_" + def __init__(self) -> None: # Text sequences: defer empty messages and reconcile streamed commits. self._open_texts: dict[str, str] = {} # key -> message_id @@ -91,6 +96,10 @@ def __init__(self) -> None: # Tool-call sequences: keep streamed arguments and commits deduplicated. self._open_tool_calls: dict[str, str] = {} # key -> tool_call_id self._seen_call_ids: set[str] = set() # emitted tool_call_ids + # Args that streamed before a START (provider skipped output_item.added): + # buffered by window key until the real call_id is known, so we never + # emit a call under a throwaway id the later commit can't reconcile. + self._pending_tool_args: dict[str, str] = {} # key -> buffered args # Reasoning sequences: track phases, parts, replay data, and reconciliation. self._open_reasonings: dict[str, str] = {} # key -> phase message_id @@ -155,6 +164,17 @@ def finalize(self) -> list[BaseEvent]: events.extend(self._close_text(key)) for key in list(self._open_tool_calls): events.extend(self._close_tool_call(key)) + # Args that streamed before a START and never got one (no output_item.added + # and no run-item commit) describe a tool call the SDK never committed — + # it is incomplete and has no known name/call_id, so drop it rather than + # surface a phantom empty-name call under a synthesized id. + for key in list(self._pending_tool_args): + delta = self._pending_tool_args.pop(key) + logger.debug( + "Dropping %d buffered arg chars for uncommitted tool call (key=%s)", + len(delta), + key, + ) for key in list(self._open_reasonings): events.extend(self._close_reasoning(key)) if self._current_step is not None: @@ -428,19 +448,22 @@ def translate_function_call_arguments_delta(self, data: Any) -> list[BaseEvent]: if not delta: return [] key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) - events: list[BaseEvent] = [] if key not in self._open_tool_calls: - # Some providers jump straight to args without an output_item.added, - # so open the call here if we haven't already. - events.extend(self._open_tool_call(key, new_tool_call_id(), "")) - events.append( + # No START yet: the provider streamed args before output_item.added, + # so the real call_id is not known here. Buffer the args; the window + # opens under the correct id at output_item.added or the run-item + # commit, and _open_tool_call flushes what we buffered. Opening under + # a throwaway id here would make the later commit fail to reconcile + # and emit a duplicate TOOL_CALL_START/ARGS/END. + self._pending_tool_args[key] = self._pending_tool_args.get(key, "") + delta + return [] + return [ ToolCallArgsEvent( type=EventType.TOOL_CALL_ARGS, tool_call_id=self._open_tool_calls[key], delta=delta, ) - ) - return events + ] def translate_reasoning_delta(self, data: Any) -> list[BaseEvent]: """Translate reasoning deltas into REASONING_MESSAGE_CONTENT (lazy part start). @@ -464,7 +487,13 @@ def translate_reasoning_delta(self, data: Any) -> list[BaseEvent]: key = self._window_key(read_attr(data, "item_id"), read_attr(data, "output_index")) events: list[BaseEvent] = [] if key not in self._open_reasonings: - events.extend(self._open_reasoning(key, new_reasoning_id())) + # Reuse the real wire id when present (mirrors output_item.added), so + # the phase/part ids and REASONING_ENCRYPTED_VALUE entity_id round-trip. + events.extend( + self._open_reasoning( + key, self._resolve_id(read_attr(data, "item_id"), new_reasoning_id) + ) + ) if key not in self._open_reasoning_parts: events.extend(self._open_reasoning_part(key)) events.append( @@ -542,7 +571,15 @@ def translate_message_output_item(self, item: MessageOutputItem) -> list[BaseEve """ raw = item.raw_item raw_id = read_attr(raw, "id") - text = ItemHelpers.extract_text(raw) or ItemHelpers.extract_refusal(raw) or "" + # Concatenate text and refusal rather than falling back: extract_text + # ignores refusals and returns truthy whenever any output_text part + # exists, so `text or refusal` would drop a refusal that streamed + # (translate_refusal_delta feeds the same window) from the snapshot. + text = "".join( + part + for part in (ItemHelpers.extract_text(raw), ItemHelpers.extract_refusal(raw)) + if part + ) action, key = self._reconcile(raw_id, self._open_texts, self._closed_text_ids) if action == "close": # key is still open at this point — read its id before _close_text pops it. @@ -614,6 +651,13 @@ def translate_tool_call_item(self, item: ToolCallItem) -> list[BaseEvent]: # no BaseEvents to emit, but the snapshot still needs this call. self._record_tool_call(call_id, name, arguments) return [] + # "new": never streamed as an open window. Any args buffered before a + # START are superseded by raw.arguments below. Clear a buffer we can + # positively correlate by real item id; a placeholder buffer we cannot + # address by id is harmlessly dropped (never re-emitted) by finalize, so + # we must NOT blind-FIFO-drop here — that could evict a *different* + # still-streaming call's buffered args. + self._pending_tool_args.pop(read_attr(raw, "id"), None) events = self._open_tool_call(call_id, call_id, name) if arguments: events.append( @@ -636,7 +680,10 @@ def translate_tool_call_output_item(self, item: ToolCallOutputItem) -> list[Base Returns: A single TOOL_CALL_RESULT event, or [] if call_id is missing. """ - call_id = item.call_id + # call_id is a property only on newer SDK ToolCallOutputItem; read it + # defensively (as translate_tool_call_item does) to support the declared + # openai-agents floor, falling back to the raw item. + call_id = getattr(item, "call_id", None) or read_attr(item.raw_item, "call_id") if not call_id: logger.debug("Tool output without call_id; skipping TOOL_CALL_RESULT") return [] @@ -672,7 +719,12 @@ def translate_reasoning_item(self, item: ReasoningItem) -> list[BaseEvent]: events.extend(self._close_reasoning(key)) return events if action == "skip": - return [] + # The phase already closed via a raw event, but the commit may be the + # only carrier of encrypted_content (some providers omit it on the raw + # done). Surface it so reasoning replay is not lost — _emit_encrypted_value + # guards against a double-emit. It arrives after REASONING_END in this + # path because the value is unknown until the commit; late beats dropped. + return self._emit_encrypted_value(key, raw) phase_id = self._resolve_id(raw_id, new_reasoning_id) events = self._open_reasoning(phase_id, phase_id) parts = [read_attr(entry, "text") for entry in (read_attr(raw, "summary") or [])] @@ -811,6 +863,16 @@ def _is_real_id(item_id: Any) -> bool: """ return bool(item_id) and item_id != FAKE_RESPONSES_ID + @classmethod + def _is_placeholder_key(cls, key: Any) -> bool: + """True for internal placeholder window keys (``__idx_*``). + + ``_is_real_id`` reports these truthy because they are neither empty nor + ``FAKE_RESPONSES_ID``, so a window-key reuse decision must also exclude + placeholders explicitly before treating the key as a wire id. + """ + return isinstance(key, str) and key.startswith(cls._PLACEHOLDER_PREFIX) + @classmethod def _resolve_id(cls, item_id: Any, generate: Any) -> str: """Return the id if real, else a freshly generated one. @@ -847,6 +909,7 @@ def _window_key(self, item_id: Any, output_index: Any, *, start: bool = False) - key in self._open_texts or key in self._pending_text_ids or key in self._open_tool_calls + or key in self._pending_tool_args or key in self._open_reasonings ) if key is None or (start and not active): @@ -917,9 +980,19 @@ def _emit_text_content(self, key: str, delta: str) -> list[BaseEvent]: return [] events: list[BaseEvent] = [] if key not in self._open_texts: - # Open deferred text on its first delta; generate an ID if added is absent. + # Open deferred text on its first delta. Reuse the real item id when + # the window key is one (honors "IDs are reused when available"); only + # synthesize an id when the key is an internal placeholder. events.extend( - self._open_text(key, self._pending_text_ids.pop(key, None) or new_message_id()) + self._open_text( + key, + self._pending_text_ids.pop(key, None) + or ( + key + if self._is_real_id(key) and not self._is_placeholder_key(key) + else new_message_id() + ), + ) ) events.append( TextMessageContentEvent( @@ -957,6 +1030,16 @@ def _open_tool_call(self, key: str, call_id: str, name: str) -> list[BaseEvent]: tool_call_name=name, ) ) + # Flush any args that streamed before this START (see _pending_tool_args). + buffered = self._pending_tool_args.pop(key, None) + if buffered: + events.append( + ToolCallArgsEvent( + type=EventType.TOOL_CALL_ARGS, + tool_call_id=call_id, + delta=buffered, + ) + ) return events def _close_tool_call(self, key: str) -> list[BaseEvent]: @@ -1050,10 +1133,14 @@ def _emit_encrypted_value(self, key: str, item: Any) -> list[BaseEvent]: or already emitted. """ encrypted = read_attr(item, "encrypted_content") - if not encrypted or key in self._emitted_encrypted_keys: - return [] - self._emitted_encrypted_keys.add(key) + # Dedup on the stable phase id, not the caller's key: the raw-done path + # passes a window key (a placeholder for FAKE-id backends) while the + # run-item skip path passes the resolved phase id. Both map to the same + # phase id here, so keying the guard on it catches the cross-path dup. entity_id = self._reasoning_phase_ids.get(key, key) + if not encrypted or entity_id in self._emitted_encrypted_keys: + return [] + self._emitted_encrypted_keys.add(entity_id) return [ ReasoningEncryptedValueEvent( type=EventType.REASONING_ENCRYPTED_VALUE, @@ -1067,7 +1154,8 @@ def _emit_encrypted_value(self, key: str, item: Any) -> list[BaseEvent]: # LEVEL 4c — Snapshot message builders # ───────────────────────────────────────────────────────────────────── # Snapshot messages reuse streamed IDs so clients can merge without duplicates. - # Reasoning is omitted because plaintext reasoning cannot be replayed. + # Reasoning is omitted from the snapshot: clients persist replayable reasoning + # from the streamed REASONING_ENCRYPTED_VALUE events, not from this list. def _record_text(self, message_id: str, text: str) -> None: """Add an assistant text message to the snapshot.""" diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py index c406633f92..b2ce67866b 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/engine/types.py @@ -30,9 +30,10 @@ class TranslatedInput(BaseModel): Returned by ``AGUITranslator.to_openai()`` (or ``AGUIToOpenAITranslator.translate()`` directly, for advanced/per-mapping - use). Fields line up one-for-one with ``ag_ui.core.RunAgentInput`` — same - names, same required/optional split — so if you know the wire format you - already know this shape. Only ``messages`` and ``tools`` are actually + use). Fields mirror ``ag_ui.core.RunAgentInput`` by name, so if you know the + wire format you already know this shape. (``parent_run_id`` and ``resume`` + are the newest core fields and are read defensively, so an older client may + not carry them.) Only ``messages`` and ``tools`` are actually translated into OpenAI Agents SDK types; everything else passes through unchanged for you to use however your app needs. @@ -74,9 +75,16 @@ class TranslatedInput(BaseModel): None for a top-level run.""" # ── Translated payload (the actual work the translator does) ──────── - messages: list[TResponseInputItem] + messages: SkipValidation[list[TResponseInputItem]] """Responses-API input items, ready to pass to Runner.run*(input=...). - Pydantic validates these against the SDK's input-item types.""" + + Validation is skipped (like ``tools``). Two reasons: (1) audio parts use the + Chat-Completions ``input_audio`` shape, which is a valid model input for an + audio-capable agent but is not a member of the Responses input-item union, so + strict validation would reject an otherwise-usable request; (2) validating a + reasoning item's ``summary`` (typed ``Iterable`` in the SDK) materializes it as + a single-use ``ValidatorIterator`` that empties after one read. The SDK + validates the shape it actually consumes at run time.""" tools: SkipValidation[list[FunctionTool]] = [] """FunctionTool proxies for the client's tools. Merge these with your diff --git a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py index efd83cb91f..ac41d1057c 100644 --- a/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py +++ b/integrations/openai-agents/python/src/ag_ui_openai_agents/translator.py @@ -196,7 +196,9 @@ async def to_agui( if emit_run_error: yield RunErrorEvent( type=EventType.RUN_ERROR, - message=run_error_message or str(exc), + # Only None means "use str(exc)"; an explicit "" is a + # deliberate (empty) message and must be honored verbatim. + message=run_error_message if run_error_message is not None else str(exc), ) raise finally: diff --git a/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py b/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py index e68dac38cf..8abab99d18 100644 --- a/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py +++ b/integrations/openai-agents/python/tests/engine/test_agui_to_openai_multimodal.py @@ -9,6 +9,7 @@ from __future__ import annotations import warnings +from types import SimpleNamespace import pytest from pydantic import ValidationError @@ -105,6 +106,9 @@ def test_audio_format_from_mime_variants(): assert fmt("audio/wav") == "wav" assert fmt("audio/wave") == "wav" assert fmt(None) == "wav" + # Content types carrying parameters must still match (params stripped). + assert fmt("audio/wav; codecs=1") == "wav" + assert fmt("audio/mpeg; rate=16000") == "mp3" # Chat Completions only takes wav/mp3 — anything else must map to None # so the caller drops the part instead of sending a rejectable request. assert fmt("audio/ogg") is None @@ -134,6 +138,7 @@ def test_document_data_source_uses_file_data(): out = _engine.translate_document_content(part) assert out == { "type": "input_file", + "filename": "document.pdf", "file_data": "data:application/pdf;base64,Zm9v", } @@ -227,11 +232,15 @@ def test_binary_as_file_url_source(): assert out == {"type": "input_file", "file_url": "https://x/y.pdf"} -def test_binary_as_file_data_without_filename_omits_key(): +def test_binary_as_file_data_without_filename_synthesizes_one(): + # Base64 file_data requires a filename (Responses API); synthesize from mime. part = _binary(mime_type="application/pdf", data="Zm9v") out = _engine._binary_as_file(part, "application/pdf") - assert out == {"type": "input_file", "file_data": "data:application/pdf;base64,Zm9v"} - assert "filename" not in out + assert out == { + "type": "input_file", + "filename": "file.pdf", + "file_data": "data:application/pdf;base64,Zm9v", + } # ── source resolution helper ────────────────────────────────────────────── @@ -376,31 +385,247 @@ def test_image_survives_translated_input_validation(): } -def test_translated_input_rejects_image_without_detail(): - with pytest.raises(ValidationError) as exc_info: - TranslatedInput( - thread_id="t1", - run_id="r1", - messages=[ - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_image", - "image_url": "https://x/y.png", - } - ], - } - ], - tools=[], - state={}, - context=[], - forwarded_props=None, - ) - - assert any( - error["loc"][-1] == "detail" - and "ResponseInputImageParam" in error["loc"] - for error in exc_info.value.errors() +def test_translated_input_skips_message_validation(): + # messages uses SkipValidation, so shapes that are not Responses input-item + # members (e.g. a detail-less image, or an input_audio block) construct + # without a ValidationError; the SDK validates what it consumes at run time. + ti = TranslatedInput( + thread_id="t1", + run_id="r1", + messages=[ + { + "type": "message", + "role": "user", + "content": [{"type": "input_image", "image_url": "https://x/y.png"}], + } + ], + tools=[], + state={}, + context=[], + forwarded_props=None, ) + assert ti.messages[0]["content"][0] == { + "type": "input_image", + "image_url": "https://x/y.png", + } + + +def test_audio_user_message_survives_to_openai(): + # input_audio is a Chat-Completions shape, not a Responses input-item member; + # with SkipValidation an audio request translates instead of crashing. + run_input = RunAgentInput( + thread_id="t1", + run_id="r1", + messages=[ + UserMessage( + id="u1", + role="user", + content=[ + AudioInputContent( + source=InputContentDataSource(value="Zm9v", mime_type="audio/wav") + ) + ], + ) + ], + tools=[], + state={}, + context=[], + forwarded_props=None, + ) + translated = AGUITranslator().to_openai(run_input) + assert translated.messages[0]["content"][0] == { + "type": "input_audio", + "input_audio": {"data": "Zm9v", "format": "wav"}, + } + + +def test_reasoning_summary_survives_as_a_stable_list_through_to_openai(): + from ag_ui.core import ReasoningMessage + + run_input = RunAgentInput( + thread_id="t1", + run_id="r1", + messages=[ + ReasoningMessage( + id="rs", role="reasoning", content="because", encrypted_value="enc" + ) + ], + tools=[], + state={}, + context=[], + forwarded_props=None, + ) + translated = AGUITranslator().to_openai(run_input) + summary = translated.messages[0]["summary"] + # A real list that survives multiple reads, not a one-shot ValidatorIterator. + assert list(summary) == [{"type": "summary_text", "text": "because"}] + assert list(summary) == [{"type": "summary_text", "text": "because"}] + + +# ── empty-content dropping ─────────────────────────────────────────────── + + +def test_empty_string_user_message_is_dropped(): + # translate_content("") yields no parts, so translate_user_message returns + # None and the turn is omitted rather than sent as an empty content part the + # API rejects (matches the method's documented contract). + assert _engine.translate_message(UserMessage(id="u1", role="user", content="")) == [] + + +def test_empty_list_user_message_is_dropped(): + assert _engine.translate_message(UserMessage(id="u1", role="user", content=[])) == [] + + +# ── defensive reads for newest RunAgentInput fields ────────────────────── + + +def test_translate_reads_resume_defensively_when_field_absent(): + # Older RunAgentInput versions lack `resume` (and `parent_run_id`). translate + # must read them via getattr and not raise AttributeError. + run_input = SimpleNamespace( + thread_id="t1", + run_id="r1", + messages=[], + tools=[], + state=None, + context=[], + forwarded_props=None, + ) + result = _engine.translate(run_input) + assert result.resume is None + assert result.parent_run_id is None + assert result.thread_id == "t1" + + +# ── document data source carries a synthesized filename ────────────────── + + +def test_document_data_source_includes_synthesized_filename(): + # The Responses API requires a filename alongside base64 file_data. + part = DocumentInputContent( + source=InputContentDataSource(value="Zm9v", mime_type="application/pdf") + ) + out = _engine.translate_document_content(part) + assert out == { + "type": "input_file", + "filename": "document.pdf", + "file_data": "data:application/pdf;base64,Zm9v", + } + + +# ── inbound message-type translation ───────────────────────────────────── + + +def test_system_and_developer_messages_map_to_role_items(): + from ag_ui.core import DeveloperMessage, SystemMessage + + assert _engine.translate_message( + SystemMessage(id="s1", role="system", content="be nice") + ) == [{"type": "message", "role": "system", "content": [{"type": "input_text", "text": "be nice"}]}] + assert _engine.translate_message( + DeveloperMessage(id="d1", role="developer", content="dev note") + ) == [{"type": "message", "role": "developer", "content": [{"type": "input_text", "text": "dev note"}]}] + + +def test_assistant_message_maps_text_and_tool_calls_without_type_message(): + from ag_ui.core import AssistantMessage, FunctionCall, ToolCall + + msg = AssistantMessage( + id="a1", + role="assistant", + content="hi", + tool_calls=[ + ToolCall(id="call_1", type="function", function=FunctionCall(name="f", arguments='{"x":1}')) + ], + ) + items = _engine.translate_message(msg) + # Prior assistant text stays an EasyInputMessageParam: NO type="message". + assert items[0] == {"role": "assistant", "content": "hi"} + assert "type" not in items[0] + assert items[1] == { + "type": "function_call", + "call_id": "call_1", + "name": "f", + "arguments": '{"x":1}', + } + + +def test_tool_message_surfaces_error_when_content_empty(): + from ag_ui.core import ToolMessage + + ok = _engine.translate_message( + ToolMessage(id="t1", role="tool", tool_call_id="call_1", content="result") + ) + assert ok[0]["output"] == "result" + + err = _engine.translate_message( + ToolMessage(id="t2", role="tool", tool_call_id="call_2", content="", error="boom") + ) + assert err[0] == {"type": "function_call_output", "call_id": "call_2", "output": "boom"} + + +def test_reasoning_message_replays_only_with_encrypted_value(): + from ag_ui.core import ReasoningMessage + + assert ( + _engine.translate_message(ReasoningMessage(id="r1", role="reasoning", content="thinking")) + == [] + ) + items = _engine.translate_message( + ReasoningMessage(id="r2", role="reasoning", content="sum", encrypted_value="enc") + ) + assert items == [ + { + "type": "reasoning", + "id": "r2", + "encrypted_content": "enc", + "summary": [{"type": "summary_text", "text": "sum"}], + } + ] + + +def test_activity_message_is_dropped(): + from ag_ui.core import ActivityMessage + + assert _engine.translate_message( + ActivityMessage(id="act1", role="activity", activity_type="typing", content={}) + ) == [] + + +def test_translate_context_renders_nonempty_items(): + from ag_ui.core import Context + + rendered = _engine.translate_context( + [ + Context(description="Language", value="German"), + Context(description="", value=""), + ] + ) + assert rendered == "Language: German" + + +# ── CR4 fixes: filename synthesis, context filtering, reasoning summary ─── + + +def test_translate_context_drops_items_missing_either_field(): + from ag_ui.core import Context + + rendered = _engine.translate_context( + [ + Context(description="Language", value="German"), + Context(description="LabelOnly", value=""), + Context(description="", value="ValueOnly"), + ] + ) + assert rendered == "Language: German" + + +def test_reasoning_message_with_empty_content_omits_summary_entry(): + from ag_ui.core import ReasoningMessage + + items = _engine.translate_message( + ReasoningMessage(id="r3", role="reasoning", content="", encrypted_value="enc") + ) + assert items == [ + {"type": "reasoning", "id": "r3", "encrypted_content": "enc", "summary": []} + ] diff --git a/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py index da4fba65ad..bf918d658e 100644 --- a/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py +++ b/integrations/openai-agents/python/tests/engine/test_openai_to_agui.py @@ -23,14 +23,18 @@ HandoffCallItem, HandoffOutputItem, MCPApprovalRequestItem, + ReasoningItem, ) from agents.items import MessageOutputItem, ToolCallItem, ToolCallOutputItem from agents.models.fake_id import FAKE_RESPONSES_ID from openai.types.responses import ( ResponseFunctionToolCall, ResponseOutputMessage, + ResponseOutputRefusal, ResponseOutputText, + ResponseReasoningItem, ) +from pydantic import BaseModel from openai.types.responses.response_output_item import McpApprovalRequest from ag_ui.core import EventType @@ -141,6 +145,9 @@ def test_text_delta_lazily_opens_window_without_output_item_added(): _raw(OpenAIRawResponseEventType.TEXT_DELTA, item_id="msg_1", output_index=0, delta="hi"), ) assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] + # The real item id must be reused even on the lazy path — a generated id here + # would make a later MessageOutputItem commit fail to reconcile and duplicate. + assert {e.message_id for e in events} == {"msg_1"} def test_text_done_does_not_close_the_message_window(): @@ -209,6 +216,28 @@ def test_finalize_closes_text_left_open_after_text_done(): assert events[0].message_id == "msg_1" +def test_message_with_text_and_refusal_records_both_in_snapshot(): + # extract_text ignores refusals, so a naive `text or refusal` would drop the + # refusal from the snapshot even though it streamed into the same window. + engine = OpenAIToAGUITranslator() + raw = ResponseOutputMessage( + id="msg_1", + type="message", + role="assistant", + status="completed", + content=[ + ResponseOutputText(type="output_text", text="Here is ", annotations=[]), + ResponseOutputRefusal(type="refusal", refusal="I can't help with that."), + ], + ) + events = _drive(engine, _run_item(MessageOutputItem(agent=_AGENT, raw_item=raw))) + streamed = "".join( + e.delta for e in events if e.type == EventType.TEXT_MESSAGE_CONTENT + ) + assert streamed == "Here is I can't help with that." + assert engine._snapshot_messages[0].content == "Here is I can't help with that." + + def test_refusal_delta_streams_into_the_text_window(): engine = OpenAIToAGUITranslator() events = _drive( @@ -297,7 +326,12 @@ def test_function_call_streams_start_args_end_under_the_call_id(): ) -def test_function_call_args_delta_lazily_opens_the_call(): +def test_function_call_args_delta_before_added_is_buffered_then_dropped_if_uncommitted(): + # A provider that streams args with no output_item.added has no call_id to + # emit yet, so the args are buffered (nothing on the wire) rather than opened + # under a throwaway id the later commit could not reconcile. With no + # output_item.added and no run-item commit ever arriving, the call was never + # completed by the SDK, so finalize drops the buffer (no phantom call). engine = OpenAIToAGUITranslator() events = _drive( engine, @@ -308,7 +342,164 @@ def test_function_call_args_delta_lazily_opens_the_call(): delta="{}", ), ) - assert _types(events) == [EventType.TOOL_CALL_START, EventType.TOOL_CALL_ARGS] + assert events == [], "premature args must buffer, not open under a throwaway id" + assert engine.finalize() == [] + + +def test_placeholder_id_args_before_added_flush_into_single_call(): + # Chat-Completions-backed (FAKE id) backend streams args before + # output_item.added, then the added arrives. The buffered args must flush into + # the same call the added opens — one call under the real call_id, no split, + # and no internal placeholder key leaked onto the wire. + engine = OpenAIToAGUITranslator() + assert ( + _drive( + engine, + _raw( + OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + item_id=FAKE_RESPONSES_ID, + output_index=0, + delta='{"a":', + ), + ) + == [] + ) + events = _drive( + engine, + _added( + OpenAIItemType.FUNCTION_CALL, + output_index=0, + id=FAKE_RESPONSES_ID, + call_id="call_1", + name="f", + ), + _raw( + OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + item_id=FAKE_RESPONSES_ID, + output_index=0, + delta="1}", + ), + _done( + OpenAIItemType.FUNCTION_CALL, + output_index=0, + id=FAKE_RESPONSES_ID, + call_id="call_1", + name="f", + ), + ) + assert _types(events) == [ + EventType.TOOL_CALL_START, + EventType.TOOL_CALL_ARGS, + EventType.TOOL_CALL_ARGS, + EventType.TOOL_CALL_END, + ] + assert {e.tool_call_id for e in events} == {"call_1"} + assert "".join(e.delta for e in events if e.type == EventType.TOOL_CALL_ARGS) == '{"a":1}' + assert engine.finalize() == [] + + +def test_placeholder_id_args_then_commit_emits_no_phantom(): + # FAKE-id backend streams args before any added, then commits via a + # ToolCallItem. The buffer (keyed by a placeholder the commit cannot address + # by id) must be consumed so finalize does not re-emit it as a phantom call. + engine = OpenAIToAGUITranslator() + assert ( + _drive( + engine, + _raw( + OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + item_id=FAKE_RESPONSES_ID, + output_index=0, + delta='{"a":1}', + ), + ) + == [] + ) + raw = ResponseFunctionToolCall( + id=FAKE_RESPONSES_ID, + type="function_call", + call_id="call_1", + name="f", + arguments='{"a":1}', + ) + commit = _drive(engine, _run_item(ToolCallItem(agent=_AGENT, raw_item=raw))) + assert _types(commit) == [ + EventType.TOOL_CALL_START, + EventType.TOOL_CALL_ARGS, + EventType.TOOL_CALL_END, + ] + assert {e.tool_call_id for e in commit} == {"call_1"} + assert engine.finalize() == [] + + +def test_placeholder_id_text_delta_synthesizes_a_clean_message_id(): + # A FAKE-id text delta with no output_item.added must synthesize a real + # generated id, never leak the internal "__idx_" placeholder window key. + engine = OpenAIToAGUITranslator() + events = _drive( + engine, + _raw( + OpenAIRawResponseEventType.TEXT_DELTA, + item_id=FAKE_RESPONSES_ID, + output_index=0, + delta="hi", + ), + ) + assert _types(events) == [EventType.TEXT_MESSAGE_START, EventType.TEXT_MESSAGE_CONTENT] + mid = events[0].message_id + assert mid.startswith("msg_") + assert not mid.startswith("__idx_") and mid != FAKE_RESPONSES_ID + + +def test_reasoning_delta_reuses_real_wire_id(): + # Reasoning that streams deltas before output_item.added must still reuse the + # real wire id for the phase (honors the id-reuse invariant), not synthesize one. + engine = OpenAIToAGUITranslator() + events = _drive( + engine, + _raw( + OpenAIRawResponseEventType.REASONING_SUMMARY_DELTA, + item_id="rs_9", + output_index=0, + delta="think", + ), + ) + starts = [e for e in events if e.type == EventType.REASONING_START] + assert starts and starts[0].message_id == "rs_9" + + +def test_args_before_added_then_run_item_commit_emits_no_duplicate(): + # Regression: args stream before output_item.added, then the semantic + # ToolCallItem commit arrives with the real call_id. Must emit exactly one + # START/ARGS/END under the real call_id — not a duplicate (buffered id + + # committed id) as the pre-fix throwaway-id path produced. + engine = OpenAIToAGUITranslator() + buffered = _drive( + engine, + _raw( + OpenAIRawResponseEventType.FUNCTION_CALL_ARGUMENTS_DELTA, + item_id="fc_1", + output_index=0, + delta='{"city":"Cairo"}', + ), + ) + assert buffered == [] + raw = ResponseFunctionToolCall( + id="fc_1", + type="function_call", + call_id="call_1", + name="get_weather", + arguments='{"city":"Cairo"}', + ) + commit = _drive(engine, _run_item(ToolCallItem(agent=_AGENT, raw_item=raw))) + assert _types(commit) == [ + EventType.TOOL_CALL_START, + EventType.TOOL_CALL_ARGS, + EventType.TOOL_CALL_END, + ] + assert {e.tool_call_id for e in commit} == {"call_1"} + # Buffer was consumed by the commit — finalize has nothing left to flush. + assert engine.finalize() == [] def test_hosted_tool_call_with_distinct_call_id_streams_once(): @@ -351,6 +542,20 @@ def test_tool_output_item_emits_tool_call_result(): assert events[0].content == "sunny" +def test_tool_output_call_id_falls_back_to_raw_item_when_property_absent(): + # Older SDK ToolCallOutputItem has no `call_id` property; the translator must + # fall back to the raw item rather than raise AttributeError. + engine = OpenAIToAGUITranslator() + item = SimpleNamespace( + raw_item={"type": "function_call_output", "call_id": "call_1", "output": "sunny"}, + output="sunny", + ) + events = engine.translate_tool_call_output_item(item) + assert _types(events) == [EventType.TOOL_CALL_RESULT] + assert events[0].tool_call_id == "call_1" + assert events[0].content == "sunny" + + # ── reasoning streaming ────────────────────────────────────────────────── @@ -451,6 +656,52 @@ def test_fake_reasoning_id_starts_fresh_when_output_index_is_reused(): assert encrypted_values == ["first", "second"] +def test_reasoning_commit_surfaces_encrypted_value_on_skip_path(): + # Reasoning streams and closes via raw events carrying no encrypted_content; + # the ReasoningItem run-item commit is then the only carrier of it. The skip + # path must still surface REASONING_ENCRYPTED_VALUE so replay data is not lost. + engine = OpenAIToAGUITranslator() + _drive( + engine, + _added(OpenAIItemType.REASONING, id="rs_1"), + _raw( + OpenAIRawResponseEventType.REASONING_TEXT_DELTA, + item_id="rs_1", + output_index=0, + delta="hmm", + ), + _raw(OpenAIRawResponseEventType.REASONING_TEXT_DONE, item_id="rs_1", output_index=0), + _done(OpenAIItemType.REASONING, id="rs_1"), # no encrypted_content here + ) + item = ReasoningItem( + agent=_AGENT, + raw_item=ResponseReasoningItem( + id="rs_1", type="reasoning", summary=[], encrypted_content="secret" + ), + ) + events = _drive(engine, _run_item(item)) + assert _types(events) == [EventType.REASONING_ENCRYPTED_VALUE] + assert events[0].encrypted_value == "secret" + + +def test_tool_output_pydantic_model_serializes_its_fields(): + # to_string should serialize a pydantic tool output to its field dict rather + # than an opaque quoted repr. + class Weather(BaseModel): + city: str + temp: int + + engine = OpenAIToAGUITranslator() + item = ToolCallOutputItem( + agent=_AGENT, + raw_item={"type": "function_call_output", "call_id": "call_1", "output": "ignored"}, + output=Weather(city="Cairo", temp=30), + ) + events = _drive(engine, _run_item(item)) + assert _types(events) == [EventType.TOOL_CALL_RESULT] + assert __import__("json").loads(events[0].content) == {"city": "Cairo", "temp": 30} + + def test_reasoning_auto_closes_when_text_output_starts(): # Once real output begins, any open reasoning must be closed first — # reasoning must never bleed into the answer window. diff --git a/integrations/openai-agents/python/tests/engine/test_openai_to_agui_snapshot.py b/integrations/openai-agents/python/tests/engine/test_openai_to_agui_snapshot.py index 030564ed8b..fdc8d10666 100644 --- a/integrations/openai-agents/python/tests/engine/test_openai_to_agui_snapshot.py +++ b/integrations/openai-agents/python/tests/engine/test_openai_to_agui_snapshot.py @@ -121,6 +121,15 @@ def test_snapshot_ids_match_streamed_event_ids(): assert call_ids == streamed_call_ids == {"call_1"} assert result_ids == streamed_result_ids == {"call_1-result"} + # Guard against duplicate emission that the set comparisons above would + # silently collapse: each id must appear on the wire exactly once. + start_ids = [e.message_id for e in streamed if e.type == EventType.TEXT_MESSAGE_START] + call_start_ids = [e.tool_call_id for e in streamed if e.type == EventType.TOOL_CALL_START] + result_event_ids = [e.message_id for e in streamed if e.type == EventType.TOOL_CALL_RESULT] + assert len(start_ids) == len(set(start_ids)) + assert len(call_start_ids) == len(set(call_start_ids)) + assert len(result_event_ids) == len(set(result_event_ids)) + # ── tool call + result round-trip ──────────────────────────────────────── @@ -230,8 +239,15 @@ def test_text_message_closed_by_raw_event_before_commit_still_snapshots(): item=SimpleNamespace(type="message", id="msg_real"), output_index=0 ) engine.translate_output_item_added(added) + # A text delta must actually open the (deferred) message window, so that + # output_item.done closes it and appends msg_real to _closed_text_ids — + # otherwise the commit reconciles as "new" and this never exercises the + # "skip" branch the regression is about. + engine.translate_text_delta( + SimpleNamespace(item_id="msg_real", output_index=0, delta="hello") + ) engine.translate_output_item_done(done) - assert engine._snapshot_messages == [] # raw close alone never records + assert engine._snapshot_messages == [] # streamed close alone never records # The run-item commit arrives after — must still record, under the # same id the raw close already used. diff --git a/integrations/openai-agents/python/tests/test_agent.py b/integrations/openai-agents/python/tests/test_agent.py index ea83a2e360..71c937b41e 100644 --- a/integrations/openai-agents/python/tests/test_agent.py +++ b/integrations/openai-agents/python/tests/test_agent.py @@ -4,7 +4,7 @@ Covers orchestration only — the event mapping itself is the translator's job and has its own coverage: -- ``run`` yields the translator-wrapped stream (RUN_STARTED first, +- ``run_streamed`` yields the translator-wrapped stream (RUN_STARTED first, RUN_FINISHED last) for one AG-UI request. - Client-declared tools are merged onto a per-request ``clone`` — the wrapped static agent is never mutated; without tools no clone happens. @@ -118,7 +118,10 @@ def confirm() -> str: sdk_agent = Agent(name="assistant", instructions="hi", tools=[confirm]) wrapper = OpenAIAgentsAgent(sdk_agent) - _collect(wrapper, _run_input(with_tool=True)) + import logging + + with caplog.at_level(logging.WARNING): + _collect(wrapper, _run_input(with_tool=True)) assert patched_runner[0]["agent"] is sdk_agent assert [tool.name for tool in sdk_agent.tools] == ["confirm"] @@ -142,6 +145,59 @@ def test_context_passes_through(patched_runner): assert patched_runner[0]["context"] == run_input.context +def test_explicit_context_overrides_agui_context(patched_runner): + # A caller that supplies its own SDK context gets exactly that object, + # not the AG-UI ambient context list. + sentinel = {"app": "context"} + run_input = _run_input() + run_input.context = [Context(description="Response language", value="German")] + wrapper = OpenAIAgentsAgent( + Agent(name="assistant", instructions="hi"), context=sentinel + ) + _collect(wrapper, run_input) + assert patched_runner[0]["context"] is sentinel + + +def test_no_context_defaults_to_none_not_empty_list(patched_runner): + # A context-less request must run with the SDK default (None), not [] — + # agents branching on `ctx.context is None` depend on this. + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi")) + _collect(wrapper, _run_input()) # _run_input has context=[] + assert patched_runner[0]["context"] is None + + +def test_explicit_context_none_is_honored(patched_runner): + # context=None is a real SDK context, distinct from "not provided". + run_input = _run_input() + run_input.context = [Context(description="x", value="y")] + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi"), context=None) + _collect(wrapper, run_input) + assert patched_runner[0]["context"] is None + + +def test_duplicate_client_tool_names_are_deduped(patched_runner, caplog): + import logging + + run_input = RunAgentInput( + thread_id="t1", + run_id="r1", + messages=[UserMessage(id="m1", role="user", content="hi")], + tools=[ + Tool(name="dup", description="a", parameters={"type": "object", "properties": {}}), + Tool(name="dup", description="b", parameters={"type": "object", "properties": {}}), + ], + state={}, + context=[], + forwarded_props=None, + ) + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi")) + with caplog.at_level(logging.WARNING): + _collect(wrapper, run_input) + ran_agent = patched_runner[0]["agent"] + assert [t.name for t in ran_agent.tools] == ["dup"], "duplicate client tool must be dropped" + assert "Ignoring duplicate client tool 'dup'" in caplog.text + + def test_to_agui_options_pass_through(patched_runner, monkeypatch): start_value = lambda: {"phase": "start"} end_value = lambda: {"phase": "end"} diff --git a/integrations/openai-agents/python/tests/test_endpoint.py b/integrations/openai-agents/python/tests/test_endpoint.py index 0f1024bbb8..c1626bcc47 100644 --- a/integrations/openai-agents/python/tests/test_endpoint.py +++ b/integrations/openai-agents/python/tests/test_endpoint.py @@ -128,3 +128,107 @@ def test_health_can_be_disabled(): routes = {(route.path, method) for route in app.routes for method in route.methods} assert ("/my_agent", "POST") in routes assert ("/my_agent/health", "GET") not in routes + + +def test_multiple_agents_on_one_app_get_unique_operation_ids(monkeypatch): + def fake_run_streamed(agent, *, input, run_config=None, **kwargs): + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _empty_stream() + return result + + monkeypatch.setattr(agent_module.Runner, "run_streamed", fake_run_streamed) + + app = FastAPI() + add_openai_agents_fastapi_endpoint( + app, OpenAIAgentsAgent(Agent(name="one", instructions="hi")), "/one" + ) + add_openai_agents_fastapi_endpoint( + app, OpenAIAgentsAgent(Agent(name="two", instructions="hi")), "/two" + ) + + # Mounting several agents must not collide on FastAPI-derived operation ids. + op_ids = [ + operation["operationId"] + for path in app.openapi()["paths"].values() + for operation in path.values() + ] + assert len(op_ids) == len(set(op_ids)), f"operation ids must be unique: {op_ids}" + + +def test_agent_error_is_logged_not_leaked_through_asgi(monkeypatch, caplog): + import logging + + def fake_run_streamed(agent, *, input, run_config=None, **kwargs): + async def boom(): + raise RuntimeError("kaboom") + yield # pragma: no cover + + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = boom() + return result + + monkeypatch.setattr(agent_module.Runner, "run_streamed", fake_run_streamed) + + app = FastAPI() + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi")) + add_openai_agents_fastapi_endpoint(app, wrapper, "/") + + # raise_server_exceptions=True (default): if the re-raise escaped the + # generator it would surface here as a 500/exception. The client must still + # receive its RUN_ERROR frame and the error must be logged, not leaked. + with caplog.at_level(logging.ERROR): + with TestClient(app).stream("POST", "/", json=RUN_INPUT_JSON) as response: + assert response.status_code == 200 + body = "".join(response.iter_text()) + + assert '"RUN_ERROR"' in body + assert "Agent run failed" in caplog.text + + +def test_setup_error_before_streaming_still_emits_run_error(monkeypatch, caplog): + import logging + + # Runner.run_streamed runs inside OpenAIAgentsAgent.run_streamed BEFORE + # to_agui, so a failure here never reaches to_agui's RUN_ERROR path. The + # endpoint must still surface a terminal RUN_ERROR rather than an empty 200. + def boom(agent, *, input, run_config=None, **kwargs): + raise RuntimeError("setup exploded") + + monkeypatch.setattr(agent_module.Runner, "run_streamed", boom) + + app = FastAPI() + wrapper = OpenAIAgentsAgent(Agent(name="assistant", instructions="hi")) + add_openai_agents_fastapi_endpoint(app, wrapper, "/") + + with caplog.at_level(logging.ERROR): + with TestClient(app).stream("POST", "/", json=RUN_INPUT_JSON) as response: + assert response.status_code == 200 + body = "".join(response.iter_text()) + + assert '"RUN_ERROR"' in body + assert "Agent run failed" in caplog.text + + +def test_colliding_path_slugs_get_unique_operation_ids(monkeypatch): + def fake_run_streamed(agent, *, input, run_config=None, **kwargs): + result = MagicMock(spec=RunResultStreaming) + result.stream_events.return_value = _empty_stream() + return result + + monkeypatch.setattr(agent_module.Runner, "run_streamed", fake_run_streamed) + + # "/a/b" and "/a_b" both normalize to the slug "a_b"; the uniqueness guard + # must still hand out distinct operation ids. + app = FastAPI() + add_openai_agents_fastapi_endpoint( + app, OpenAIAgentsAgent(Agent(name="one", instructions="hi")), "/a/b" + ) + add_openai_agents_fastapi_endpoint( + app, OpenAIAgentsAgent(Agent(name="two", instructions="hi")), "/a_b" + ) + op_ids = [ + operation["operationId"] + for path in app.openapi()["paths"].values() + for operation in path.values() + ] + assert len(op_ids) == len(set(op_ids)), f"operation ids must be unique: {op_ids}" diff --git a/integrations/openai-agents/python/tests/test_translator.py b/integrations/openai-agents/python/tests/test_translator.py index 1615572ebc..dfca53d340 100644 --- a/integrations/openai-agents/python/tests/test_translator.py +++ b/integrations/openai-agents/python/tests/test_translator.py @@ -355,6 +355,31 @@ async def collect(): assert error.message == "Agent run failed" +def test_to_agui_empty_run_error_message_is_honored_not_treated_as_unset(): + # An explicit "" means "send an empty message" (suppress the raw text), only + # None falls back to str(exc). Guards against leaking exception detail. + class _ExplodingOutbound(_StubOutbound): + def translate(self, openai_event): + raise RuntimeError("raw internal detail") + + translator = AGUITranslator(outbound_cls=_ExplodingOutbound) + collected: list = [] + + async def collect(): + async for e in translator.to_agui( + _fake_stream("a"), _run_input(), run_error_message="" + ): + collected.append(e) + + with pytest.raises(RuntimeError, match="raw internal detail"): + asyncio.run(collect()) + + error = collected[-1] + assert isinstance(error, RunErrorEvent) + assert error.message == "" + assert "raw internal detail" not in error.message + + def test_to_agui_closes_open_windows_before_run_error(): # A client keying its teardown on *_END would spin forever on the # half-streamed message if the error path skipped the flush.