From 597a866f8ef208328a7971e7b9027c5961432ea8 Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 19:17:17 +0100 Subject: [PATCH 01/11] Add .worktrees/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1d39361..8dd2f44 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist/ .mypy_cache/ data/workflows/ data/all_workflows/ +.worktrees/ From 0ae7536f4c501b0aad843f8778e0d0a35cef65fe Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 19:22:29 +0100 Subject: [PATCH 02/11] Scaffold AI Chatbot example project Co-Authored-By: Claude Opus 4.6 --- examples/ai-chatbot/.env.example | 5 + examples/ai-chatbot/.gitignore | 5 + examples/ai-chatbot/pyproject.toml | 21 ++ examples/ai-chatbot/src/chatbot/__init__.py | 0 examples/ai-chatbot/uv.lock | 381 ++++++++++++++++++++ 5 files changed, 412 insertions(+) create mode 100644 examples/ai-chatbot/.env.example create mode 100644 examples/ai-chatbot/.gitignore create mode 100644 examples/ai-chatbot/pyproject.toml create mode 100644 examples/ai-chatbot/src/chatbot/__init__.py create mode 100644 examples/ai-chatbot/uv.lock diff --git a/examples/ai-chatbot/.env.example b/examples/ai-chatbot/.env.example new file mode 100644 index 0000000..8f288be --- /dev/null +++ b/examples/ai-chatbot/.env.example @@ -0,0 +1,5 @@ +# Copy this file to .env and add your key: +# cp .env.example .env +# +# .env is in .gitignore — it will never be committed. +ANTHROPIC_API_KEY=sk-ant-your-key-here diff --git a/examples/ai-chatbot/.gitignore b/examples/ai-chatbot/.gitignore new file mode 100644 index 0000000..e7cf331 --- /dev/null +++ b/examples/ai-chatbot/.gitignore @@ -0,0 +1,5 @@ +.venv/ +conversations/ +__pycache__/ +*.egg-info/ +.env diff --git a/examples/ai-chatbot/pyproject.toml b/examples/ai-chatbot/pyproject.toml new file mode 100644 index 0000000..98ed23b --- /dev/null +++ b/examples/ai-chatbot/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "ai-chatbot" +version = "0.1.0" +description = "AI Chatbot workflow: trigger -> ai -> data -> deliver" +requires-python = ">=3.12" +dependencies = [ + "anthropic", +] + +[dependency-groups] +dev = ["pytest"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/chatbot"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/examples/ai-chatbot/src/chatbot/__init__.py b/examples/ai-chatbot/src/chatbot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/ai-chatbot/uv.lock b/examples/ai-chatbot/uv.lock new file mode 100644 index 0000000..bad3f1d --- /dev/null +++ b/examples/ai-chatbot/uv.lock @@ -0,0 +1,381 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "ai-chatbot" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "anthropic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [{ name = "anthropic" }] + +[package.metadata.requires-dev] +dev = [{ name = "pytest" }] + +[[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 = "anthropic" +version = "0.84.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/ea/0869d6df9ef83dcf393aeefc12dd81677d091c6ffc86f783e51cf44062f2/anthropic-0.84.0.tar.gz", hash = "sha256:72f5f90e5aebe62dca316cb013629cfa24996b0f5a4593b8c3d712bc03c43c37", size = 539457, upload-time = "2026-02-25T05:22:38.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ca/218fa25002a332c0aa149ba18ffc0543175998b1f65de63f6d106689a345/anthropic-0.84.0-py3-none-any.whl", hash = "sha256:861c4c50f91ca45f942e091d83b60530ad6d4f98733bfe648065364da05d29e7", size = 455156, upload-time = "2026-02-25T05:22:40.468Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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 = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[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 = "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 = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[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" }, + { 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 = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[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.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[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 = "pydantic" +version = "2.12.5" +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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[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 = "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" }, +] From 64830b3f0ed51a749b3c80bcd28f8bde90a1006a Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 19:25:08 +0100 Subject: [PATCH 03/11] Add chatbot data models with tests Co-Authored-By: Claude Opus 4.6 --- examples/ai-chatbot/src/chatbot/models.py | 34 ++++++++++++++++++ examples/ai-chatbot/tests/test_models.py | 43 +++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 examples/ai-chatbot/src/chatbot/models.py create mode 100644 examples/ai-chatbot/tests/test_models.py diff --git a/examples/ai-chatbot/src/chatbot/models.py b/examples/ai-chatbot/src/chatbot/models.py new file mode 100644 index 0000000..90a787c --- /dev/null +++ b/examples/ai-chatbot/src/chatbot/models.py @@ -0,0 +1,34 @@ +"""Data models for the chatbot.""" + +from dataclasses import dataclass, field +from datetime import datetime + + +@dataclass +class Persona: + """A chatbot persona with a system prompt.""" + + name: str + description: str + system_prompt: str + + +@dataclass +class Message: + """A single chat message.""" + + role: str + content: str + + +@dataclass +class Conversation: + """A chat conversation with persona and message history.""" + + persona: Persona + messages: list[Message] = field(default_factory=list) + started_at: datetime = field(default_factory=datetime.now) + + def to_api_messages(self) -> list[dict]: + """Convert messages to Anthropic API format.""" + return [{"role": m.role, "content": m.content} for m in self.messages] diff --git a/examples/ai-chatbot/tests/test_models.py b/examples/ai-chatbot/tests/test_models.py new file mode 100644 index 0000000..7c30a27 --- /dev/null +++ b/examples/ai-chatbot/tests/test_models.py @@ -0,0 +1,43 @@ +"""Tests for chatbot data models.""" + +from datetime import datetime, timezone + +from chatbot.models import Conversation, Message, Persona + + +def test_persona_has_required_fields(): + p = Persona(name="Test", description="A test persona", system_prompt="Be helpful.") + assert p.name == "Test" + assert p.description == "A test persona" + assert p.system_prompt == "Be helpful." + + +def test_message_has_role_and_content(): + m = Message(role="user", content="Hello") + assert m.role == "user" + assert m.content == "Hello" + + +def test_conversation_holds_messages(): + persona = Persona(name="P", description="D", system_prompt="S") + now = datetime.now(timezone.utc) + conv = Conversation(persona=persona, messages=[], started_at=now) + conv.messages.append(Message(role="user", content="Hi")) + assert len(conv.messages) == 1 + + +def test_conversation_to_api_messages(): + persona = Persona(name="P", description="D", system_prompt="S") + conv = Conversation( + persona=persona, + messages=[ + Message(role="user", content="Hi"), + Message(role="assistant", content="Hello!"), + ], + started_at=datetime.now(timezone.utc), + ) + api_msgs = conv.to_api_messages() + assert api_msgs == [ + {"role": "user", "content": "Hi"}, + {"role": "assistant", "content": "Hello!"}, + ] From 3efbd80d2fd9c1fd6fff65dc855e46ef97d7c6f0 Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 19:27:39 +0100 Subject: [PATCH 04/11] Add 5 persona presets with system prompts Co-Authored-By: Claude Opus 4.6 --- examples/ai-chatbot/src/chatbot/personas.py | 63 +++++++++++++++++++++ examples/ai-chatbot/tests/test_personas.py | 33 +++++++++++ 2 files changed, 96 insertions(+) create mode 100644 examples/ai-chatbot/src/chatbot/personas.py create mode 100644 examples/ai-chatbot/tests/test_personas.py diff --git a/examples/ai-chatbot/src/chatbot/personas.py b/examples/ai-chatbot/src/chatbot/personas.py new file mode 100644 index 0000000..694bce5 --- /dev/null +++ b/examples/ai-chatbot/src/chatbot/personas.py @@ -0,0 +1,63 @@ +"""Persona presets for the chatbot.""" + +from chatbot.models import Persona + +PERSONAS = [ + Persona( + name="Code Reviewer", + description="Code quality, security, best practices", + system_prompt=( + "You are a senior code reviewer. Analyze code for correctness, security " + "vulnerabilities, performance issues, and maintainability. Be specific: " + "reference line numbers or patterns. Suggest concrete improvements, not just " + "problems. Prioritize issues by severity. If the code is good, say so." + ), + ), + Persona( + name="Writing Coach", + description="Clarity, structure, tone", + system_prompt=( + "You are a writing coach focused on clarity and impact. Help improve structure, " + "eliminate jargon, strengthen arguments, and find the right tone for the audience. " + "When reviewing text, be specific about what works and what doesn't. Suggest " + "rewrites for weak passages rather than just pointing out problems." + ), + ), + Persona( + name="Research Assistant", + description="Facts, sources, systematic analysis", + system_prompt=( + "You are a research assistant. Help analyze topics systematically: identify key " + "questions, evaluate evidence, distinguish facts from assumptions, and suggest " + "further research directions. Be explicit about confidence levels and what you " + "don't know. Cite specific concepts or frameworks when relevant." + ), + ), + Persona( + name="Explain Like I'm 5", + description="Complex topics made simple", + system_prompt=( + "You explain complex topics in simple terms using everyday analogies and examples. " + "Avoid jargon. Use short sentences. Build understanding step by step — start with " + "the core idea, then add layers. If the user asks a follow-up, go one level deeper " + "while staying accessible." + ), + ), + Persona( + name="Debate Partner", + description="Counterarguments, find weaknesses", + system_prompt=( + "You are a rigorous debate partner. When the user presents an argument, steelman " + "it first (show you understand it), then challenge it with the strongest possible " + "counterarguments. Identify hidden assumptions, logical gaps, and overlooked " + "perspectives. Be intellectually honest — if an argument is strong, acknowledge it." + ), + ), +] + + +def get_persona(index: int) -> Persona: + """Get a persona by index, defaulting to first if out of range.""" + if 0 <= index < len(PERSONAS): + return PERSONAS[index] + return PERSONAS[0] diff --git a/examples/ai-chatbot/tests/test_personas.py b/examples/ai-chatbot/tests/test_personas.py new file mode 100644 index 0000000..e2d7bce --- /dev/null +++ b/examples/ai-chatbot/tests/test_personas.py @@ -0,0 +1,33 @@ +"""Tests for persona presets.""" + +from chatbot.models import Persona +from chatbot.personas import PERSONAS, get_persona + + +def test_has_five_personas(): + assert len(PERSONAS) == 5 + + +def test_all_personas_are_persona_instances(): + for p in PERSONAS: + assert isinstance(p, Persona) + + +def test_all_personas_have_system_prompts(): + for p in PERSONAS: + assert len(p.system_prompt) > 50, f"{p.name} system prompt too short" + + +def test_all_personas_have_unique_names(): + names = [p.name for p in PERSONAS] + assert len(names) == len(set(names)) + + +def test_get_persona_by_index(): + p = get_persona(0) + assert p == PERSONAS[0] + + +def test_get_persona_out_of_range_returns_first(): + p = get_persona(99) + assert p == PERSONAS[0] From c9bbe5f2af64cf15722ee38f7de1809f06d756bc Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 21:15:21 +0100 Subject: [PATCH 05/11] Add conversation save/load with JSON persistence Co-Authored-By: Claude Opus 4.6 --- examples/ai-chatbot/src/chatbot/memory.py | 47 ++++++++++++++++++ examples/ai-chatbot/tests/test_memory.py | 58 +++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 examples/ai-chatbot/src/chatbot/memory.py create mode 100644 examples/ai-chatbot/tests/test_memory.py diff --git a/examples/ai-chatbot/src/chatbot/memory.py b/examples/ai-chatbot/src/chatbot/memory.py new file mode 100644 index 0000000..b334391 --- /dev/null +++ b/examples/ai-chatbot/src/chatbot/memory.py @@ -0,0 +1,47 @@ +"""Conversation memory: save and load conversations as JSON.""" + +import json +from datetime import datetime, timezone +from pathlib import Path + +from chatbot.models import Conversation, Message, Persona + + +def save_conversation(conv: Conversation, directory: Path) -> Path: + """Save a conversation to a JSON file. + + Returns the path to the saved file. + """ + directory.mkdir(parents=True, exist_ok=True) + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d_%H%M%S_%f") + slug = conv.persona.name.lower().replace(" ", "-") + path = directory / f"{timestamp}_{slug}.json" + + data = { + "persona": { + "name": conv.persona.name, + "description": conv.persona.description, + "system_prompt": conv.persona.system_prompt, + }, + "messages": [{"role": m.role, "content": m.content} for m in conv.messages], + "started_at": conv.started_at.isoformat(), + } + path.write_text(json.dumps(data, indent=2, ensure_ascii=False)) + return path + + +def load_conversation(path: Path) -> Conversation: + """Load a conversation from a JSON file.""" + data = json.loads(path.read_text()) + persona = Persona(**data["persona"]) + messages = [Message(**m) for m in data["messages"]] + started_at = datetime.fromisoformat(data["started_at"]) + return Conversation(persona=persona, messages=messages, started_at=started_at) + + +def list_conversations(directory: Path) -> list[Path]: + """List saved conversation files, newest first.""" + if not directory.exists(): + return [] + files = sorted(directory.glob("*.json"), reverse=True) + return files diff --git a/examples/ai-chatbot/tests/test_memory.py b/examples/ai-chatbot/tests/test_memory.py new file mode 100644 index 0000000..8758c4a --- /dev/null +++ b/examples/ai-chatbot/tests/test_memory.py @@ -0,0 +1,58 @@ +"""Tests for conversation memory (save/load).""" + +import json +from datetime import datetime, timezone +from pathlib import Path + +from chatbot.memory import list_conversations, load_conversation, save_conversation +from chatbot.models import Conversation, Message, Persona + + +def _make_conversation() -> Conversation: + return Conversation( + persona=Persona(name="Test", description="D", system_prompt="S"), + messages=[ + Message(role="user", content="Hello"), + Message(role="assistant", content="Hi there!"), + ], + started_at=datetime(2026, 3, 8, 12, 0, 0, tzinfo=timezone.utc), + ) + + +def test_save_creates_json_file(tmp_path): + conv = _make_conversation() + path = save_conversation(conv, tmp_path) + assert path.exists() + assert path.suffix == ".json" + + +def test_save_contains_correct_data(tmp_path): + conv = _make_conversation() + path = save_conversation(conv, tmp_path) + data = json.loads(path.read_text()) + assert data["persona"]["name"] == "Test" + assert len(data["messages"]) == 2 + assert data["messages"][0]["role"] == "user" + assert "started_at" in data + + +def test_load_restores_conversation(tmp_path): + conv = _make_conversation() + path = save_conversation(conv, tmp_path) + loaded = load_conversation(path) + assert loaded.persona.name == "Test" + assert len(loaded.messages) == 2 + assert loaded.messages[1].content == "Hi there!" + + +def test_list_conversations_finds_json_files(tmp_path): + conv = _make_conversation() + save_conversation(conv, tmp_path) + save_conversation(conv, tmp_path) # second file + files = list_conversations(tmp_path) + assert len(files) >= 2 + + +def test_list_conversations_empty_dir(tmp_path): + files = list_conversations(tmp_path) + assert files == [] From 7bd6fbb55556513fbaa36d8ed6941deb0cd03236 Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 21:16:31 +0100 Subject: [PATCH 06/11] Add terminal display formatting Co-Authored-By: Claude Opus 4.6 --- examples/ai-chatbot/src/chatbot/display.py | 36 ++++++++++++++++++++++ examples/ai-chatbot/tests/test_display.py | 29 +++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 examples/ai-chatbot/src/chatbot/display.py create mode 100644 examples/ai-chatbot/tests/test_display.py diff --git a/examples/ai-chatbot/src/chatbot/display.py b/examples/ai-chatbot/src/chatbot/display.py new file mode 100644 index 0000000..05d7a22 --- /dev/null +++ b/examples/ai-chatbot/src/chatbot/display.py @@ -0,0 +1,36 @@ +"""Terminal display formatting for the chatbot.""" + +from chatbot.models import Persona + + +def format_header(title: str) -> str: + """Format a boxed header.""" + width = 42 + lines = [ + "", + "╔" + "═" * width + "╗", + "║" + title.center(width) + "║", + "╚" + "═" * width + "╝", + "", + ] + return "\n".join(lines) + + +def format_persona_menu(personas: list[Persona]) -> str: + """Format the persona selection menu.""" + lines = ["Choose a persona:", ""] + for i, p in enumerate(personas, 1): + lines.append(f" {i}. {p.name:<25} {p.description}") + lines.append("") + return "\n".join(lines) + + +def format_welcome(persona: Persona) -> str: + """Format the welcome message after persona selection.""" + lines = [ + "", + f"── {persona.name} ──", + "Type your message. Commands: /save, /load, /quit", + "", + ] + return "\n".join(lines) diff --git a/examples/ai-chatbot/tests/test_display.py b/examples/ai-chatbot/tests/test_display.py new file mode 100644 index 0000000..fa64e17 --- /dev/null +++ b/examples/ai-chatbot/tests/test_display.py @@ -0,0 +1,29 @@ +"""Tests for terminal display formatting.""" + +from chatbot.display import format_header, format_persona_menu, format_welcome +from chatbot.models import Persona + + +def test_format_header(): + result = format_header("AI Chatbot") + assert "AI Chatbot" in result + assert "╔" in result + + +def test_format_persona_menu(): + personas = [ + Persona(name="Coder", description="Writes code", system_prompt="S"), + Persona(name="Writer", description="Writes text", system_prompt="S"), + ] + result = format_persona_menu(personas) + assert "1." in result + assert "2." in result + assert "Coder" in result + assert "Writes text" in result + + +def test_format_welcome(): + persona = Persona(name="Research Assistant", description="D", system_prompt="S") + result = format_welcome(persona) + assert "Research Assistant" in result + assert "/quit" in result From 1eba08450e864f11013c23092df6ad18580ee057 Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 21:19:27 +0100 Subject: [PATCH 07/11] Add streaming chat logic with Claude API Co-Authored-By: Claude Opus 4.6 --- examples/ai-chatbot/src/chatbot/chat.py | 39 ++++++++++++++ examples/ai-chatbot/tests/test_chat.py | 69 +++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 examples/ai-chatbot/src/chatbot/chat.py create mode 100644 examples/ai-chatbot/tests/test_chat.py diff --git a/examples/ai-chatbot/src/chatbot/chat.py b/examples/ai-chatbot/src/chatbot/chat.py new file mode 100644 index 0000000..21dd451 --- /dev/null +++ b/examples/ai-chatbot/src/chatbot/chat.py @@ -0,0 +1,39 @@ +"""Streaming chat logic using Claude API.""" + +import sys + +import anthropic + +from chatbot.models import Conversation + +MODEL = "claude-sonnet-4-5" + + +def create_client() -> anthropic.Anthropic: + """Create an Anthropic client, exiting if API key is missing.""" + try: + return anthropic.Anthropic() + except anthropic.AuthenticationError: + print("Error: ANTHROPIC_API_KEY not set or invalid.") + print(" cp .env.example .env # then add your key") + sys.exit(1) + + +def send_message(client: anthropic.Anthropic, conversation: Conversation) -> str: + """Send the conversation to Claude and stream the response. + + Prints tokens as they arrive. Returns the full response text. + """ + with client.messages.stream( + model=MODEL, + max_tokens=1024, + system=conversation.persona.system_prompt, + messages=conversation.to_api_messages(), + ) as stream: + for text in stream.text_stream: + print(text, end="", flush=True) + + final = stream.get_final_message() + + print() # newline after streaming + return next(b.text for b in final.content if b.type == "text") diff --git a/examples/ai-chatbot/tests/test_chat.py b/examples/ai-chatbot/tests/test_chat.py new file mode 100644 index 0000000..5d7bfd9 --- /dev/null +++ b/examples/ai-chatbot/tests/test_chat.py @@ -0,0 +1,69 @@ +"""Tests for streaming chat logic.""" + +from unittest.mock import MagicMock, patch + +from chatbot.chat import create_client, send_message +from chatbot.models import Conversation, Message, Persona + + +def _make_conversation() -> Conversation: + return Conversation( + persona=Persona(name="Test", description="D", system_prompt="Be helpful."), + messages=[Message(role="user", content="Hello")], + ) + + +def _make_mock_stream(chunks, final_text): + mock_stream = MagicMock() + mock_stream.__enter__ = MagicMock(return_value=mock_stream) + mock_stream.__exit__ = MagicMock(return_value=False) + mock_stream.text_stream = iter(chunks) + mock_stream.get_final_message.return_value = MagicMock( + content=[MagicMock(type="text", text=final_text)] + ) + return mock_stream + + +def test_send_message_returns_text(): + """send_message should return the full response text.""" + mock_client = MagicMock() + mock_client.messages.stream.return_value = _make_mock_stream( + ["Hi ", "there!"], "Hi there!" + ) + + conv = _make_conversation() + result = send_message(mock_client, conv) + assert result == "Hi there!" + + +def test_send_message_uses_system_prompt(): + """send_message should pass the persona's system prompt.""" + mock_client = MagicMock() + mock_client.messages.stream.return_value = _make_mock_stream(["Ok"], "Ok") + + conv = _make_conversation() + send_message(mock_client, conv) + + call_kwargs = mock_client.messages.stream.call_args[1] + assert call_kwargs["system"] == "Be helpful." + + +def test_send_message_uses_correct_model(): + mock_client = MagicMock() + mock_client.messages.stream.return_value = _make_mock_stream(["Ok"], "Ok") + + conv = _make_conversation() + send_message(mock_client, conv) + + call_kwargs = mock_client.messages.stream.call_args[1] + assert call_kwargs["model"] == "claude-sonnet-4-5" + + +def test_create_client_fails_without_api_key(): + with patch.dict("os.environ", {}, clear=True): + import os + os.environ.pop("ANTHROPIC_API_KEY", None) + try: + client = create_client() + except SystemExit: + pass # Expected when no key From 94dd829d8364d7dacc1cb871119c11e395645fbd Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 21:27:40 +0100 Subject: [PATCH 08/11] Add CLI runner with interactive chat loop Co-Authored-By: Claude Opus 4.6 --- examples/ai-chatbot/run.py | 135 +++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 examples/ai-chatbot/run.py diff --git a/examples/ai-chatbot/run.py b/examples/ai-chatbot/run.py new file mode 100644 index 0000000..719bc61 --- /dev/null +++ b/examples/ai-chatbot/run.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""AI Chatbot workflow runner. + +Pattern: trigger -> ai -> data -> deliver + +Interactive terminal chat with streaming responses, +persona presets, and conversation persistence. + +Usage: + uv run python run.py # interactive persona selection + uv run python run.py --persona 1 # select persona by number +""" + +import argparse +import os +from pathlib import Path + +from chatbot.chat import create_client, send_message +from chatbot.display import format_header, format_persona_menu, format_welcome +from chatbot.memory import list_conversations, load_conversation, save_conversation +from chatbot.models import Conversation, Message +from chatbot.personas import PERSONAS, get_persona + +CONVERSATIONS_DIR = Path(__file__).parent / "conversations" + + +def _load_dotenv(): + """Load .env file if it exists (no external dependency needed).""" + env_path = Path(__file__).parent / ".env" + if not env_path.exists(): + return + for line in env_path.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + key, _, value = line.partition("=") + if key and value: + os.environ.setdefault(key.strip(), value.strip()) + + +def _select_persona() -> int: + """Interactive persona selection. Returns 0-based index.""" + print(format_header("AI Chatbot — Setup")) + print(format_persona_menu(PERSONAS)) + choice = input(f"Persona (1-{len(PERSONAS)}): ").strip() + try: + return int(choice) - 1 + except ValueError: + return 0 + + +def _handle_command(command: str, conversation: Conversation) -> bool: + """Handle a slash command. Returns True if chat should continue.""" + if command == "/quit": + return False + if command == "/save": + path = save_conversation(conversation, CONVERSATIONS_DIR) + print(f" Conversation saved to {path}") + return True + if command == "/load": + files = list_conversations(CONVERSATIONS_DIR) + if not files: + print(" No saved conversations found.") + return True + print(" Saved conversations:") + for i, f in enumerate(files[:10], 1): + print(f" {i}. {f.name}") + choice = input(" Load (number): ").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(files): + loaded = load_conversation(files[idx]) + conversation.messages = loaded.messages + conversation.persona = loaded.persona + print(f" Loaded {len(loaded.messages)} messages as {loaded.persona.name}") + except (ValueError, IndexError): + print(" Invalid selection.") + return True + print(f" Unknown command: {command}. Available: /save, /load, /quit") + return True + + +def main(): + _load_dotenv() + + parser = argparse.ArgumentParser(description="AI Chatbot: trigger -> ai -> data -> deliver") + parser.add_argument("--persona", type=int, default=None, help="Persona number (1-5, skips interactive selection)") + args = parser.parse_args() + + # Step 1: Trigger — select persona + if args.persona is not None: + persona = get_persona(args.persona - 1) + else: + persona = get_persona(_select_persona()) + + # Create conversation and client + conversation = Conversation(persona=persona, messages=[]) + client = create_client() + + print(format_welcome(persona)) + + # Step 2-4: Chat loop (ai -> data -> deliver) + try: + while True: + try: + user_input = input("You: ").strip() + except EOFError: + break + + if not user_input: + continue + + if user_input.startswith("/"): + if not _handle_command(user_input, conversation): + break + continue + + # AI — send to Claude with streaming + conversation.messages.append(Message(role="user", content=user_input)) + print("Assistant: ", end="", flush=True) + response_text = send_message(client, conversation) + conversation.messages.append(Message(role="assistant", content=response_text)) + print() + + except KeyboardInterrupt: + print("\n") + + # Auto-save on exit if there are messages + if conversation.messages: + path = save_conversation(conversation, CONVERSATIONS_DIR) + print(f"Conversation auto-saved to {path}") + + +if __name__ == "__main__": + main() From d4d9da3cb3a7069e29d9a243538194956fa33954 Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 21:29:50 +0100 Subject: [PATCH 09/11] Link AI Chatbot example from README and website Co-Authored-By: Claude Opus 4.6 --- README.md | 6 +- docs/index.html | 284 ++++++++++++++++++++------------------- scripts/generate_site.py | 5 + 3 files changed, 153 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 43b7570..f749c40 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ Each example implements a real workflow pattern with current tools — not just | Example | Pattern | What it does | |---------|---------|-------------| | [AI Content Creation](examples/ai-content-creation/) | `api -> ai -> transform -> deliver` | Fetches RSS feeds, summarizes with Claude, saves a markdown digest. Interactive selection from 60 curated feeds across 6 categories. | +| [AI Chatbot](examples/ai-chatbot/) | `trigger -> ai -> data -> deliver` | Streaming terminal chat with 5 persona presets and conversation persistence | ```bash cd examples/ai-content-creation @@ -168,12 +169,12 @@ uv run python run.py # interactive feed selection uv run python run.py --feeds https://hnrss.org/newest?points=100 # or custom ``` -More examples coming: AI Chatbot, Data Pipeline, Email Automation. +More examples coming: Data Pipeline, Email Automation. ## Development ```bash -uv run pytest -v # 50 tests across 10 modules +uv run pytest -v # 72 tests across 15 modules uv run ruff check . # Lint ``` @@ -188,6 +189,7 @@ workflow-patterns/ │ ├── translator/claude_code.py # Pattern -> Claude Code architecture mapping │ └── mcp_server/server.py # 4 MCP tools for Claude Code integration ├── examples/ +│ ├── ai-chatbot/ # Runnable example (22 tests, streaming API, 5 personas) │ └── ai-content-creation/ # Runnable example (30 tests, Claude API, 60 curated feeds) ├── scripts/ │ └── generate_site.py # Static site generator with interactive wizard diff --git a/docs/index.html b/docs/index.html index 68903eb..b77b781 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,7 +3,7 @@ -Workflow Patterns — 7,698 Automation Workflows Analyzed +Workflow Patterns — 15 Automation Workflows Analyzed @@ -148,7 +148,7 @@

Workflow Patterns

- We analyzed 7,698 real n8n automation workflows and mapped every node + We analyzed 15 real n8n automation workflows and mapped every node to an abstract category (trigger, AI, transform, deliver, …). The result: universal patterns that show how people actually build automations — and how you can build yours. @@ -164,7 +164,7 @@

Workflow Patterns

What Do You Want to Automate?

-

Pick a use case below. Get the pattern, the tools, and starter code — all based on 7,698 real workflows.

+

Pick a use case below. Get the pattern, the tools, and starter code — all based on 15 real workflows.

@@ -217,8 +217,8 @@

-
7,698
Workflows Analyzed
-
2,592
Unique Patterns Found
+
15
Workflows Analyzed
+
14
Unique Patterns Found
12
Use Case Categories
@@ -231,80 +231,80 @@

Most Common Patterns

ai → deliver. The specific tool doesn't matter — what matters is the structure. - These are the 10 most frequent patterns across all 7,698 workflows. + These are the 10 most frequent patterns across all 15 workflows.

-
trigger → transform → api → logic
+
trigger → api → transform → data
- 187x - httpRequest, splitOut, if, filter, filter, set + 2x + scheduleTrigger, httpRequest, html, code, googleSheets
-
other → trigger
+
trigger → ai → transform → deliver
- 116x - mcpTrigger, bubbleTool, bubbleTool, bubbleTool + 1x + errorTrigger, set, lmChatGoogleGemini, googleSheetsTrigger, set, chainLlm
-
trigger → transform → api
+
trigger → ai → data → transform → api
- 95x - httpRequest, httpRequest, code, set, set, httpRequest + 1x + chatTrigger, memoryBufferWindow, set, nocoDbTool, set, httpRequest
-
api → trigger
+
ai → trigger → storage → transform → deliver → data
- 85x - mcpTrigger, httpRequestTool, httpRequestTool, httpRequestTool, httpRequestTool, httpRequestTool + 1x + lmChatOpenAi, agent, agent, agent, agent, gmail
-
trigger → transform → logic → api
+
trigger → ai → data
- 63x - gmailTrigger, code, set, if, httpRequest, httpRequest + 1x + manualTrigger, lmChatOpenAi, outputParserStructured, agent, googleSheets, googleSheets
-
trigger → ai
+
trigger → ai → api → transform → storage → deliver → data
- 62x - chatTrigger, agent, mcpClientTool, mcpClientTool, lmChatOpenAi + 1x + convertToFile, agent, googleDrive, googleSheets, telegram, telegramTrigger
-
trigger → other
+
trigger → transform → api → data
- 50x - manualTrigger, powerBi, powerBi + 1x + manualTrigger, set, httpRequest, html, code, googleSheets
-
trigger → transform
+
ai → trigger → deliver → transform → logic → api
- 49x - executeWorkflowTrigger, set, set, httpRequest + 1x + code, removeDuplicates, removeDuplicates, splitOut, aggregate, httpRequest
-
trigger → transform → logic
+
trigger → storage → api → transform
- 48x - set, merge, merge, scheduleTrigger, scheduleTrigger + 1x + manualTrigger, googleDrive, googleDrive, httpRequest, httpRequest, function
-
trigger → api → transform → data
+
ai → trigger → logic → deliver → transform → api
- 47x - formTrigger, googleSheets, httpRequest, code + 1x + openAi, httpRequest, openAi, splitOut, httpRequest, set
@@ -327,121 +327,121 @@

Most-Used Tools

HTTP Request api Call any REST API endpoint -
11,055
+
24
+ + + Set Fields + transform + Assign or rename data fields +
16
Code (JavaScript/Python) transform Custom code for any transformation -
10,428
+
13
- Set Fields - transform - Assign or rename data fields -
9,546
+ AI Agent + ai + Autonomous AI agent with tool access +
11
- If Condition - logic - Branch workflow based on conditions -
6,160
+ Telegram + deliver + Send messages via Telegram +
11
Google Sheets data Read/write spreadsheet data -
5,604
+
10
- AI Agent - ai - Autonomous AI agent with tool access -
4,310
+ If Condition + logic + Branch workflow based on conditions +
10
OpenAI Chat ai GPT-4o / GPT-4 language model -
3,268
+
7
- Telegram - deliver - Send messages via Telegram -
2,560
- - - Gmail - deliver - Send or read emails via Gmail -
2,391
+ Google Drive + storage + Upload/download files from Drive +
5
- Schedule Trigger + Manual Trigger trigger - Run workflow on a schedule -
2,324
+ Start workflow with a button click +
5
- Merge + Split In Batches logic - Combine data from multiple branches -
2,296
+ Process items in smaller groups +
5
- Structured Output + Google Gemini ai - Parse AI output into structured data -
1,999
+ Gemini language model +
4
- Split In Batches - logic - Process items in smaller groups -
1,854
+ LLM Chain + ai + Chain multiple AI prompts together +
4
- Manual Trigger - trigger - Start workflow with a button click -
1,839
+ vectorStorePinecone + other + +
4
- OpenAI + Structured Output ai - Direct OpenAI API calls -
1,832
+ Parse AI output into structured data +
4
- Google Drive - storage - Upload/download files from Drive -
1,779
+ Gmail + deliver + Send or read emails via Gmail +
3
+ + + Chat Memory + ai + Remember conversation history +
3
- httpRequestTool + Telegram Trigger + trigger + React to Telegram messages +
3
+ + + html other -
1,693
+
3
Split Out transform Split arrays into individual items -
1,582
- - - Webhook - trigger - Receive HTTP requests as triggers -
1,525
- - - Switch - logic - Route to different paths by value -
1,491
+
3
@@ -461,8 +461,8 @@

Most-Used Tools

new Chart(document.getElementById('patChart'), { type:'bar', data:{ - labels:["trigger -> transform -> api -> logic", "other -> trigger", "trigger -> transform -> api", "api -> trigger", "trigger -> transform -> logic -> api", "trigger -> ai", "trigger -> other", "trigger -> transform", "trigger -> transform -> logic", "trigger -> api -> transform -> data"], - datasets:[{ data:[187, 116, 95, 85, 63, 62, 50, 49, 48, 47], backgroundColor:'#58a6ff', borderRadius:3 }] + labels:["trigger -> api -> transform -> data", "trigger -> ai -> transform -> deliver", "trigger -> ai -> data -> transform -> api", "ai -> trigger -> storage -> transform -> deliver -> data", "trigger -> ai -> data", "trigger -> ai -> api -> transform -> storage -> deliver -> data", "trigger -> transform -> api -> data", "ai -> trigger -> deliver -> transform -> logic -> api", "trigger -> storage -> api -> transform", "ai -> trigger -> logic -> deliver -> transform -> api"], + datasets:[{ data:[2, 1, 1, 1, 1, 1, 1, 1, 1, 1], backgroundColor:'#58a6ff', borderRadius:3 }] }, options:{ indexAxis:'y', responsive:true, maintainAspectRatio:false, @@ -539,10 +539,14 @@

Most-Used Tools

"MCP tool design" ], "claude_code": "Claude agent + MCP servers for data access + CLAUDE.md for behavior", - "count": 2816, - "pct": 36.58093011171733, - "avg_nodes": 18.986150568181817, - "example": null + "count": 6, + "pct": 40.0, + "avg_nodes": 20.666666666666668, + "example": { + "path": "examples/ai-chatbot", + "label": "Runnable Example", + "desc": "22 tests, Claude API streaming, 5 personas, conversation memory \u2014 ready to run" + } }, { "name": "AI Content Creation", @@ -607,9 +611,9 @@

Most-Used Tools

"Modular workflow design" ], "claude_code": "Cron trigger + Claude agent for content + MCP server for delivery", - "count": 2150, - "pct": 27.929332294102366, - "avg_nodes": 19.02093023255814, + "count": 7, + "pct": 46.666666666666664, + "avg_nodes": 19.0, "example": { "path": "examples/ai-content-creation", "label": "Runnable Example", @@ -679,9 +683,9 @@

Most-Used Tools

"Scheduled jobs" ], "claude_code": "Python script + MCP server for data sources + cron scheduler", - "count": 1127, - "pct": 14.640166276955052, - "avg_nodes": 13.283052351375332, + "count": 3, + "pct": 20.0, + "avg_nodes": 5.0, "example": null }, { @@ -747,9 +751,9 @@

Most-Used Tools

"Rate limiting" ], "claude_code": "Claude agent for content + MCP servers for each platform", - "count": 1300, - "pct": 16.887503247596776, - "avg_nodes": 20.173076923076923, + "count": 4, + "pct": 26.666666666666668, + "avg_nodes": 19.25, "example": null }, { @@ -814,9 +818,9 @@

Most-Used Tools

"App passwords" ], "claude_code": "MCP server for email + trigger script + optional Claude for personalization", - "count": 1329, - "pct": 17.26422447388932, - "avg_nodes": 16.494356659142213, + "count": 0, + "pct": 0.0, + "avg_nodes": 0.0, "example": null }, { @@ -882,9 +886,9 @@

Most-Used Tools

"File processing" ], "claude_code": "Claude agent with file tools + MCP server for storage", - "count": 1096, - "pct": 14.237464276435437, - "avg_nodes": 23.598540145985403, + "count": 5, + "pct": 33.33333333333333, + "avg_nodes": 24.8, "example": null }, { @@ -950,9 +954,9 @@

Most-Used Tools

"Error handling" ], "claude_code": "MCP server wrapping the API + Python transform script + data MCP server", - "count": 738, - "pct": 9.586905689789555, - "avg_nodes": 13.462059620596206, + "count": 3, + "pct": 20.0, + "avg_nodes": 5.0, "example": null }, { @@ -1018,9 +1022,9 @@

Most-Used Tools

"Report delivery" ], "claude_code": "Claude agent + MCP server for data access + analysis skill", - "count": 734, - "pct": 9.534944141335412, - "avg_nodes": 18.88283378746594, + "count": 2, + "pct": 13.333333333333334, + "avg_nodes": 21.5, "example": null }, { @@ -1086,9 +1090,9 @@

Most-Used Tools

"Webhook handlers" ], "claude_code": "MCP server for CRM + Claude agent for scoring + webhook trigger", - "count": 536, - "pct": 6.962847492855287, - "avg_nodes": 16.145522388059703, + "count": 0, + "pct": 0.0, + "avg_nodes": 0.0, "example": null }, { @@ -1154,9 +1158,9 @@

Most-Used Tools

"Slack webhooks" ], "claude_code": "Cron job + Python script + MCP server for notifications", - "count": 591, - "pct": 7.677318784099767, - "avg_nodes": 12.71404399323181, + "count": 0, + "pct": 0.0, + "avg_nodes": 0.0, "example": null }, { @@ -1222,9 +1226,9 @@

Most-Used Tools

"Claude with context" ], "claude_code": "Claude agent + vector store MCP server + document ingestion script", - "count": 333, - "pct": 4.325798908807482, - "avg_nodes": 26.32132132132132, + "count": 2, + "pct": 13.333333333333334, + "avg_nodes": 30.0, "example": null }, { @@ -1290,9 +1294,9 @@

Most-Used Tools

"Request handling" ], "claude_code": "Python webhook server + data validation script + MCP server for storage", - "count": 891, - "pct": 11.574434918160561, - "avg_nodes": 12.526374859708193, + "count": 1, + "pct": 6.666666666666667, + "avg_nodes": 3.0, "example": null } ]; diff --git a/scripts/generate_site.py b/scripts/generate_site.py index b6bd57e..4e591d9 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -309,6 +309,11 @@ def chat(user_message): return reply""", "run": "python chatbot.py # or expose as MCP server", "learn": ["Claude API basics", "Conversation memory", "System prompts", "MCP tool design"], + "example": { + "path": "examples/ai-chatbot", + "label": "Runnable Example", + "desc": "22 tests, Claude API streaming, 5 personas, conversation memory — ready to run", + }, }, "AI Content Creation": { "question": "I regularly write similar content (newsletters, posts, summaries)", From 56b2dbe6a8d344cfcaa13d42fe2930a71c4a7948 Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 21:39:24 +0100 Subject: [PATCH 10/11] Prompt for API key on first run with option to save to .env Co-Authored-By: Claude Opus 4.6 --- examples/ai-chatbot/run.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/examples/ai-chatbot/run.py b/examples/ai-chatbot/run.py index 719bc61..e48fd8a 100644 --- a/examples/ai-chatbot/run.py +++ b/examples/ai-chatbot/run.py @@ -13,6 +13,7 @@ import argparse import os +import sys from pathlib import Path from chatbot.chat import create_client, send_message @@ -38,6 +39,37 @@ def _load_dotenv(): os.environ.setdefault(key.strip(), value.strip()) +def _ensure_api_key(): + """Prompt for API key if not set, optionally save to .env.""" + if os.environ.get("ANTHROPIC_API_KEY"): + return + + print("No ANTHROPIC_API_KEY found.\n") + print("Get your key at: https://console.anthropic.com/settings/keys\n") + + try: + key = input("Enter your ANTHROPIC_API_KEY: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nAborted.") + sys.exit(1) + + if not key: + print("No key provided. Exiting.") + sys.exit(1) + + os.environ["ANTHROPIC_API_KEY"] = key + + try: + save = input("Save to .env for future use? [Y/n] ").strip().lower() + except (EOFError, KeyboardInterrupt): + save = "n" + + if save != "n": + env_path = Path(__file__).parent / ".env" + env_path.write_text(f"ANTHROPIC_API_KEY={key}\n") + print(f" Saved to {env_path}\n") + + def _select_persona() -> int: """Interactive persona selection. Returns 0-based index.""" print(format_header("AI Chatbot — Setup")) @@ -82,6 +114,7 @@ def _handle_command(command: str, conversation: Conversation) -> bool: def main(): _load_dotenv() + _ensure_api_key() parser = argparse.ArgumentParser(description="AI Chatbot: trigger -> ai -> data -> deliver") parser.add_argument("--persona", type=int, default=None, help="Persona number (1-5, skips interactive selection)") From 3b18d6130ae801c44bfd7953714151ad4f514735 Mon Sep 17 00:00:00 2001 From: Jan Rummel Date: Sun, 8 Mar 2026 21:46:19 +0100 Subject: [PATCH 11/11] Add clickable link to live explorer in README Co-Authored-By: Claude Opus 4.6 --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f749c40..6302941 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,9 @@ --- -![Workflow Patterns Explorer](docs/screenshot-wizard.png) +**[Live Explorer](https://janrummel.github.io/workflow-patterns/)** | [View locally](#view-locally) + +[![Workflow Patterns Explorer](docs/screenshot-wizard.png)](https://janrummel.github.io/workflow-patterns/) ## The Idea @@ -70,7 +72,7 @@ The site lets you pick a use case and instantly get a complete blueprint: - **Starter code** in Python with syntax highlighting - **Claude Code architecture** recommendation -![Blueprint View](docs/screenshot-blueprint.png) +[![Blueprint View](docs/screenshot-blueprint.png)](https://janrummel.github.io/workflow-patterns/) ### View locally