From fc198e822e249d8d94a84ae1f298b4e7319abfcb Mon Sep 17 00:00:00 2001 From: KomoriDev Date: Sat, 21 Feb 2026 12:27:30 +0800 Subject: [PATCH 1/3] :sparkles: add llm matchers --- .gitignore | 4 + entari.yml | 5 +- pdm.lock | 424 ++++++++++++------ pyproject.toml | 1 + src/entari_plugin_llm/__init__.py | 8 +- .../{callback.py => _callback.py} | 0 src/entari_plugin_llm/_handler.py | 281 ++++++++++++ src/entari_plugin_llm/_jsondata.py | 54 +++ src/entari_plugin_llm/config.py | 19 +- src/entari_plugin_llm/exception.py | 2 + src/entari_plugin_llm/listeners/__init__.py | 108 ++--- src/entari_plugin_llm/matchers/__init__.py | 176 ++++++++ src/entari_plugin_llm/matchers/utils.py | 60 +++ src/entari_plugin_llm/model.py | 66 +++ src/entari_plugin_llm/service.py | 2 +- 15 files changed, 1012 insertions(+), 198 deletions(-) rename src/entari_plugin_llm/{callback.py => _callback.py} (100%) create mode 100644 src/entari_plugin_llm/_handler.py create mode 100644 src/entari_plugin_llm/_jsondata.py create mode 100644 src/entari_plugin_llm/exception.py create mode 100644 src/entari_plugin_llm/matchers/__init__.py create mode 100644 src/entari_plugin_llm/matchers/utils.py create mode 100644 src/entari_plugin_llm/model.py diff --git a/.gitignore b/.gitignore index 29c6762..ae467c6 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,7 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ + +.entari/ +.vscode/ +data.db diff --git a/entari.yml b/entari.yml index 0dbd112..77d7b67 100644 --- a/entari.yml +++ b/entari.yml @@ -7,6 +7,7 @@ basic: ignores: - httpx._client - httpcore._trace + - aiosqlite.core prefix: - / schema: true @@ -18,12 +19,11 @@ plugins: .record_message: record_send: true ::echo: {} - ::help: {} ::inspect: {} ::auto_reload: watch_config: true entari_plugin_llm: - api_key: ${{ env.LLM_API_KEY }} + api_key: ${{ env.OPENAI_API_KEY }} base_url: https://api.openai.com/v1 models: - name: "deepseek/deepseek-chat" @@ -33,6 +33,7 @@ plugins: api_key: ${{ env.GEMINI_API_KEY }} base_url: https://generativelanguage.googleapis.com - name: "gpt-4.1-mini" + alias: "gpt" prompt: | 你是一个由用户提供信息并回答问题的智能助手。请根据用户提供的信息,尽可能准确和简洁地回答他们的问题。如果信息不足以回答问题,请礼貌地告知用户需要更多信息。 diff --git a/pdm.lock b/pdm.lock index 81f2d6b..62cad3e 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:ff340616d8fcc5a267d3cd5ace1c6421f959841a06d1f0f2faf056852ddbd475" +content_hash = "sha256:c7e07beaeed1772abbfdca468a2b20eae68238f4907568fdd5064d3c374d2736" [[metadata.targets]] requires_python = ">=3.11" @@ -144,6 +144,36 @@ files = [ {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, ] +[[package]] +name = "aiosqlite" +version = "0.22.1" +requires_python = ">=3.9" +summary = "asyncio bridge to the standard sqlite3 module" +groups = ["default"] +marker = "python_version >= \"3.11\"" +files = [ + {file = "aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb"}, + {file = "aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650"}, +] + +[[package]] +name = "alembic" +version = "1.18.4" +requires_python = ">=3.10" +summary = "A database migration tool for SQLAlchemy." +groups = ["default"] +marker = "python_version >= \"3.11\"" +dependencies = [ + "Mako", + "SQLAlchemy>=1.4.23", + "tomli; python_version < \"3.11\"", + "typing-extensions>=4.12", +] +files = [ + {file = "alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a"}, + {file = "alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc"}, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -459,6 +489,27 @@ files = [ {file = "docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912"}, ] +[[package]] +name = "entari-plugin-database" +version = "0.2.3" +requires_python = ">=3.10" +summary = "Entari plugin for SQLAlchemy ORM" +groups = ["default"] +marker = "python_version >= \"3.11\"" +dependencies = [ + "aiosqlite>=0.21.0", + "alembic>=1.16.5", + "arclet-entari<0.18.0,>=0.17.0rc1", + "arclet-letoderea>=0.19.5", + "graia-amnesia>=0.11.4", + "sqlalchemy>=2.0.42", + "tarina<0.8.0,>=0.7.1", +] +files = [ + {file = "entari_plugin_database-0.2.3-py3-none-any.whl", hash = "sha256:2599f46f6eb9ebb2532f79102256679ccb33148463c3f0d6a21f02d3cc708625"}, + {file = "entari_plugin_database-0.2.3.tar.gz", hash = "sha256:bb265018209ce2fe7196f9365a38d742d3aba3a316519258f56ab6975c7287d1"}, +] + [[package]] name = "entari-plugin-server" version = "0.6.0" @@ -532,14 +583,14 @@ files = [ [[package]] name = "filelock" -version = "3.24.2" +version = "3.24.3" requires_python = ">=3.10" summary = "A platform independent file lock." groups = ["default"] marker = "python_version >= \"3.11\"" files = [ - {file = "filelock-3.24.2-py3-none-any.whl", hash = "sha256:667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556"}, - {file = "filelock-3.24.2.tar.gz", hash = "sha256:c22803117490f156e59fafce621f0550a7a853e2bbf4f87f112b11d469b6c81b"}, + {file = "filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d"}, + {file = "filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa"}, ] [[package]] @@ -698,6 +749,61 @@ files = [ {file = "graia_amnesia-0.11.5.tar.gz", hash = "sha256:5cb3ff867205e1ee40dbb71499eb55a1d1475a642d77abddd732b2feb30d8350"}, ] +[[package]] +name = "greenlet" +version = "3.3.2" +requires_python = ">=3.10" +summary = "Lightweight in-process concurrent programming" +groups = ["default"] +marker = "(platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version >= \"3.11\"" +files = [ + {file = "greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be"}, + {file = "greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5"}, + {file = "greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd"}, + {file = "greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395"}, + {file = "greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f"}, + {file = "greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643"}, + {file = "greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b"}, + {file = "greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124"}, + {file = "greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327"}, + {file = "greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5"}, + {file = "greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492"}, + {file = "greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71"}, + {file = "greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e"}, + {file = "greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a"}, + {file = "greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2"}, +] + [[package]] name = "h11" version = "0.16.0" @@ -1015,6 +1121,21 @@ files = [ {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, ] +[[package]] +name = "mako" +version = "1.3.10" +requires_python = ">=3.8" +summary = "A super-fast templating language that borrows the best ideas from the existing templating languages." +groups = ["default"] +marker = "python_version >= \"3.11\"" +dependencies = [ + "MarkupSafe>=0.9.2", +] +files = [ + {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, + {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1655,109 +1776,109 @@ files = [ [[package]] name = "regex" -version = "2026.1.15" -requires_python = ">=3.9" +version = "2026.2.19" +requires_python = ">=3.10" summary = "Alternative regular expression module, to replace re." groups = ["default"] marker = "python_version >= \"3.11\"" files = [ - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"}, - {file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"}, - {file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"}, - {file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"}, - {file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"}, - {file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"}, - {file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"}, - {file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"}, - {file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"}, - {file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"}, - {file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"}, - {file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"}, - {file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"}, - {file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"}, - {file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"}, - {file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"}, - {file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"}, - {file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"}, - {file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"}, - {file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"}, + {file = "regex-2026.2.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:93b16a18cadb938f0f2306267161d57eb33081a861cee9ffcd71e60941eb5dfc"}, + {file = "regex-2026.2.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78af1e499cab704131f6f4e2f155b7f54ce396ca2acb6ef21a49507e4752e0be"}, + {file = "regex-2026.2.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eb20c11aa4c3793c9ad04c19a972078cdadb261b8429380364be28e867a843f2"}, + {file = "regex-2026.2.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db5fd91eec71e7b08de10011a2223d0faa20448d4e1380b9daa179fa7bf58906"}, + {file = "regex-2026.2.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fdbade8acba71bb45057c2b72f477f0b527c4895f9c83e6cfc30d4a006c21726"}, + {file = "regex-2026.2.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:31a5f561eb111d6aae14202e7043fb0b406d3c8dddbbb9e60851725c9b38ab1d"}, + {file = "regex-2026.2.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4584a3ee5f257b71e4b693cc9be3a5104249399f4116fe518c3f79b0c6fc7083"}, + {file = "regex-2026.2.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:196553ba2a2f47904e5dc272d948a746352e2644005627467e055be19d73b39e"}, + {file = "regex-2026.2.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0c10869d18abb759a3317c757746cc913d6324ce128b8bcec99350df10419f18"}, + {file = "regex-2026.2.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e689fed279cbe797a6b570bd18ff535b284d057202692c73420cb93cca41aa32"}, + {file = "regex-2026.2.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0782bd983f19ac7594039c9277cd6f75c89598c1d72f417e4d30d874105eb0c7"}, + {file = "regex-2026.2.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:dbb240c81cfed5d4a67cb86d7676d9f7ec9c3f186310bec37d8a1415210e111e"}, + {file = "regex-2026.2.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80d31c3f1fe7e4c6cd1831cd4478a0609903044dfcdc4660abfe6fb307add7f0"}, + {file = "regex-2026.2.19-cp311-cp311-win32.whl", hash = "sha256:66e6a43225ff1064f8926adbafe0922b370d381c3330edaf9891cade52daa790"}, + {file = "regex-2026.2.19-cp311-cp311-win_amd64.whl", hash = "sha256:59a7a5216485a1896c5800e9feb8ff9213e11967b482633b6195d7da11450013"}, + {file = "regex-2026.2.19-cp311-cp311-win_arm64.whl", hash = "sha256:ec661807ffc14c8d14bb0b8c1bb3d5906e476bc96f98b565b709d03962ee4dd4"}, + {file = "regex-2026.2.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1665138776e4ac1aa75146669236f7a8a696433ec4e525abf092ca9189247cc"}, + {file = "regex-2026.2.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d792b84709021945597e05656aac059526df4e0c9ef60a0eaebb306f8fafcaa8"}, + {file = "regex-2026.2.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db970bcce4d63b37b3f9eb8c893f0db980bbf1d404a1d8d2b17aa8189de92c53"}, + {file = "regex-2026.2.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03d706fbe7dfec503c8c3cb76f9352b3e3b53b623672aa49f18a251a6c71b8e6"}, + {file = "regex-2026.2.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dbff048c042beef60aa1848961384572c5afb9e8b290b0f1203a5c42cf5af65"}, + {file = "regex-2026.2.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccaaf9b907ea6b4223d5cbf5fa5dff5f33dc66f4907a25b967b8a81339a6e332"}, + {file = "regex-2026.2.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75472631eee7898e16a8a20998d15106cb31cfde21cdf96ab40b432a7082af06"}, + {file = "regex-2026.2.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d89f85a5ccc0cec125c24be75610d433d65295827ebaf0d884cbe56df82d4774"}, + {file = "regex-2026.2.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9f81806abdca3234c3dd582b8a97492e93de3602c8772013cb4affa12d1668"}, + {file = "regex-2026.2.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9dadc10d1c2bbb1326e572a226d2ec56474ab8aab26fdb8cf19419b372c349a9"}, + {file = "regex-2026.2.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6bc25d7e15f80c9dc7853cbb490b91c1ec7310808b09d56bd278fe03d776f4f6"}, + {file = "regex-2026.2.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:965d59792f5037d9138da6fed50ba943162160443b43d4895b182551805aff9c"}, + {file = "regex-2026.2.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:38d88c6ed4a09ed61403dbdf515d969ccba34669af3961ceb7311ecd0cef504a"}, + {file = "regex-2026.2.19-cp312-cp312-win32.whl", hash = "sha256:5df947cabab4b643d4791af5e28aecf6bf62e6160e525651a12eba3d03755e6b"}, + {file = "regex-2026.2.19-cp312-cp312-win_amd64.whl", hash = "sha256:4146dc576ea99634ae9c15587d0c43273b4023a10702998edf0fa68ccb60237a"}, + {file = "regex-2026.2.19-cp312-cp312-win_arm64.whl", hash = "sha256:cdc0a80f679353bd68450d2a42996090c30b2e15ca90ded6156c31f1a3b63f3b"}, + {file = "regex-2026.2.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8df08decd339e8b3f6a2eb5c05c687fe9d963ae91f352bc57beb05f5b2ac6879"}, + {file = "regex-2026.2.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3aa0944f1dc6e92f91f3b306ba7f851e1009398c84bfd370633182ee4fc26a64"}, + {file = "regex-2026.2.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c13228fbecb03eadbfd8f521732c5fda09ef761af02e920a3148e18ad0e09968"}, + {file = "regex-2026.2.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d0e72703c60d68b18b27cde7cdb65ed2570ae29fb37231aa3076bfb6b1d1c13"}, + {file = "regex-2026.2.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46e69a4bf552e30e74a8aa73f473c87efcb7f6e8c8ece60d9fd7bf13d5c86f02"}, + {file = "regex-2026.2.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8edda06079bd770f7f0cf7f3bba1a0b447b96b4a543c91fe0c142d034c166161"}, + {file = "regex-2026.2.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cbc69eae834afbf634f7c902fc72ff3e993f1c699156dd1af1adab5d06b7fe7"}, + {file = "regex-2026.2.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bcf57d30659996ee5c7937999874504c11b5a068edc9515e6a59221cc2744dd1"}, + {file = "regex-2026.2.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8e6e77cd92216eb489e21e5652a11b186afe9bdefca8a2db739fd6b205a9e0a4"}, + {file = "regex-2026.2.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b9ab8dec42afefa6314ea9b31b188259ffdd93f433d77cad454cd0b8d235ce1c"}, + {file = "regex-2026.2.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:294c0fb2e87c6bcc5f577c8f609210f5700b993151913352ed6c6af42f30f95f"}, + {file = "regex-2026.2.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c0924c64b082d4512b923ac016d6e1dcf647a3560b8a4c7e55cbbd13656cb4ed"}, + {file = "regex-2026.2.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:790dbf87b0361606cb0d79b393c3e8f4436a14ee56568a7463014565d97da02a"}, + {file = "regex-2026.2.19-cp313-cp313-win32.whl", hash = "sha256:43cdde87006271be6963896ed816733b10967baaf0e271d529c82e93da66675b"}, + {file = "regex-2026.2.19-cp313-cp313-win_amd64.whl", hash = "sha256:127ea69273485348a126ebbf3d6052604d3c7da284f797bba781f364c0947d47"}, + {file = "regex-2026.2.19-cp313-cp313-win_arm64.whl", hash = "sha256:5e56c669535ac59cbf96ca1ece0ef26cb66809990cda4fa45e1e32c3b146599e"}, + {file = "regex-2026.2.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93d881cab5afdc41a005dba1524a40947d6f7a525057aa64aaf16065cf62faa9"}, + {file = "regex-2026.2.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:80caaa1ddcc942ec7be18427354f9d58a79cee82dea2a6b3d4fd83302e1240d7"}, + {file = "regex-2026.2.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d793c5b4d2b4c668524cd1651404cfc798d40694c759aec997e196fe9729ec60"}, + {file = "regex-2026.2.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5100acb20648d9efd3f4e7e91f51187f95f22a741dcd719548a6cf4e1b34b3f"}, + {file = "regex-2026.2.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e3a31e94d10e52a896adaa3adf3621bd526ad2b45b8c2d23d1bbe74c7423007"}, + {file = "regex-2026.2.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8497421099b981f67c99eba4154cf0dfd8e47159431427a11cfb6487f7791d9e"}, + {file = "regex-2026.2.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e7a08622f7d51d7a068f7e4052a38739c412a3e74f55817073d2e2418149619"}, + {file = "regex-2026.2.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8abe671cf0f15c26b1ad389bf4043b068ce7d3b1c5d9313e12895f57d6738555"}, + {file = "regex-2026.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5a8f28dd32a4ce9c41758d43b5b9115c1c497b4b1f50c457602c1d571fa98ce1"}, + {file = "regex-2026.2.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:654dc41a5ba9b8cc8432b3f1aa8906d8b45f3e9502442a07c2f27f6c63f85db5"}, + {file = "regex-2026.2.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4a02faea614e7fdd6ba8b3bec6c8e79529d356b100381cec76e638f45d12ca04"}, + {file = "regex-2026.2.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d96162140bb819814428800934c7b71b7bffe81fb6da2d6abc1dcca31741eca3"}, + {file = "regex-2026.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c227f2922153ee42bbeb355fd6d009f8c81d9d7bdd666e2276ce41f53ed9a743"}, + {file = "regex-2026.2.19-cp313-cp313t-win32.whl", hash = "sha256:a178df8ec03011153fbcd2c70cb961bc98cbbd9694b28f706c318bee8927c3db"}, + {file = "regex-2026.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:2c1693ca6f444d554aa246b592355b5cec030ace5a2729eae1b04ab6e853e768"}, + {file = "regex-2026.2.19-cp313-cp313t-win_arm64.whl", hash = "sha256:c0761d7ae8d65773e01515ebb0b304df1bf37a0a79546caad9cbe79a42c12af7"}, + {file = "regex-2026.2.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:03d191a9bcf94d31af56d2575210cb0d0c6a054dbcad2ea9e00aa4c42903b919"}, + {file = "regex-2026.2.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:516ee067c6c721d0d0bfb80a2004edbd060fffd07e456d4e1669e38fe82f922e"}, + {file = "regex-2026.2.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:997862c619994c4a356cb7c3592502cbd50c2ab98da5f61c5c871f10f22de7e5"}, + {file = "regex-2026.2.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b9e1b8a7ebe2807cd7bbdf662510c8e43053a23262b9f46ad4fc2dfc9d204e"}, + {file = "regex-2026.2.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6c8fb3b19652e425ff24169dad3ee07f99afa7996caa9dfbb3a9106cd726f49a"}, + {file = "regex-2026.2.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50f1ee9488dd7a9fda850ec7c68cad7a32fa49fd19733f5403a3f92b451dcf73"}, + {file = "regex-2026.2.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab780092b1424d13200aa5a62996e95f65ee3db8509be366437439cdc0af1a9f"}, + {file = "regex-2026.2.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17648e1a88e72d88641b12635e70e6c71c5136ba14edba29bf8fc6834005a265"}, + {file = "regex-2026.2.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f914ae8c804c8a8a562fe216100bc156bfb51338c1f8d55fe32cf407774359a"}, + {file = "regex-2026.2.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c7e121a918bbee3f12ac300ce0a0d2f2c979cf208fb071ed8df5a6323281915c"}, + {file = "regex-2026.2.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2fedd459c791da24914ecc474feecd94cf7845efb262ac3134fe27cbd7eda799"}, + {file = "regex-2026.2.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ea8dfc99689240e61fb21b5fc2828f68b90abf7777d057b62d3166b7c1543c4c"}, + {file = "regex-2026.2.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fff45852160960f29e184ec8a5be5ab4063cfd0b168d439d1fc4ac3744bf29e"}, + {file = "regex-2026.2.19-cp314-cp314-win32.whl", hash = "sha256:5390b130cce14a7d1db226a3896273b7b35be10af35e69f1cca843b6e5d2bb2d"}, + {file = "regex-2026.2.19-cp314-cp314-win_amd64.whl", hash = "sha256:e581f75d5c0b15669139ca1c2d3e23a65bb90e3c06ba9d9ea194c377c726a904"}, + {file = "regex-2026.2.19-cp314-cp314-win_arm64.whl", hash = "sha256:7187fdee1be0896c1499a991e9bf7c78e4b56b7863e7405d7bb687888ac10c4b"}, + {file = "regex-2026.2.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5ec1d7c080832fdd4e150c6f5621fe674c70c63b3ae5a4454cebd7796263b175"}, + {file = "regex-2026.2.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8457c1bc10ee9b29cdfd897ccda41dce6bde0e9abd514bcfef7bcd05e254d411"}, + {file = "regex-2026.2.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cce8027010d1ffa3eb89a0b19621cdc78ae548ea2b49fea1f7bfb3ea77064c2b"}, + {file = "regex-2026.2.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11c138febb40546ff9e026dbbc41dc9fb8b29e61013fa5848ccfe045f5b23b83"}, + {file = "regex-2026.2.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:74ff212aa61532246bb3036b3dfea62233414b0154b8bc3676975da78383cac3"}, + {file = "regex-2026.2.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d00c95a2b6bfeb3ea1cb68d1751b1dfce2b05adc2a72c488d77a780db06ab867"}, + {file = "regex-2026.2.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:311fcccb76af31be4c588d5a17f8f1a059ae8f4b097192896ebffc95612f223a"}, + {file = "regex-2026.2.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77cfd6b5e7c4e8bf7a39d243ea05882acf5e3c7002b0ef4756de6606893b0ecd"}, + {file = "regex-2026.2.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6380f29ff212ec922b6efb56100c089251940e0526a0d05aa7c2d9b571ddf2fe"}, + {file = "regex-2026.2.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:655f553a1fa3ab8a7fd570eca793408b8d26a80bfd89ed24d116baaf13a38969"}, + {file = "regex-2026.2.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:015088b8558502f1f0bccd58754835aa154a7a5b0bd9d4c9b7b96ff4ae9ba876"}, + {file = "regex-2026.2.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9e6693b8567a59459b5dda19104c4a4dbbd4a1c78833eacc758796f2cfef1854"}, + {file = "regex-2026.2.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4071209fd4376ab5ceec72ad3507e9d3517c59e38a889079b98916477a871868"}, + {file = "regex-2026.2.19-cp314-cp314t-win32.whl", hash = "sha256:2905ff4a97fad42f2d0834d8b1ea3c2f856ec209837e458d71a061a7d05f9f01"}, + {file = "regex-2026.2.19-cp314-cp314t-win_amd64.whl", hash = "sha256:64128549b600987e0f335c2365879895f860a9161f283b14207c800a6ed623d3"}, + {file = "regex-2026.2.19-cp314-cp314t-win_arm64.whl", hash = "sha256:a09ae430e94c049dc6957f6baa35ee3418a3a77f3c12b6e02883bd80a2b679b0"}, + {file = "regex-2026.2.19.tar.gz", hash = "sha256:6fb8cb09b10e38f3ae17cc6dc04a1df77762bd0351b6ba9041438e7cc85ec310"}, ] [[package]] @@ -1780,7 +1901,7 @@ files = [ [[package]] name = "rich" -version = "14.3.2" +version = "14.3.3" requires_python = ">=3.8.0" summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" groups = ["default"] @@ -1790,8 +1911,8 @@ dependencies = [ "pygments<3.0.0,>=2.13.0", ] files = [ - {file = "rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69"}, - {file = "rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"}, + {file = "rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}, + {file = "rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}, ] [[package]] @@ -1919,30 +2040,30 @@ files = [ [[package]] name = "ruff" -version = "0.15.1" +version = "0.15.2" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["dev"] marker = "python_version >= \"3.11\"" files = [ - {file = "ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a"}, - {file = "ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602"}, - {file = "ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454"}, - {file = "ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c"}, - {file = "ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330"}, - {file = "ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61"}, - {file = "ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f"}, - {file = "ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098"}, - {file = "ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336"}, - {file = "ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416"}, - {file = "ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f"}, + {file = "ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d"}, + {file = "ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e"}, + {file = "ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87"}, + {file = "ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9"}, + {file = "ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80"}, + {file = "ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f"}, + {file = "ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77"}, + {file = "ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea"}, + {file = "ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a"}, + {file = "ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956"}, + {file = "ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4"}, + {file = "ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de"}, + {file = "ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c"}, + {file = "ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8"}, + {file = "ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f"}, + {file = "ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5"}, + {file = "ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e"}, + {file = "ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342"}, ] [[package]] @@ -2052,6 +2173,59 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.46" +requires_python = ">=3.7" +summary = "Database Abstraction Library" +groups = ["default"] +marker = "python_version >= \"3.11\"" +dependencies = [ + "greenlet>=1; platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\"", + "importlib-metadata; python_version < \"3.8\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330"}, + {file = "sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e"}, + {file = "sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7"}, +] + [[package]] name = "starlette" version = "0.52.1" diff --git a/pyproject.toml b/pyproject.toml index 173eaec..4760518 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [ "openai>=2.14.0", "docstring-parser>=0.17.0", "litellm>=1.80.9", + "entari-plugin-database>=0.2.3", ] requires-python = ">=3.10" readme = "README.md" diff --git a/src/entari_plugin_llm/__init__.py b/src/entari_plugin_llm/__init__.py index 288704a..b9d455d 100644 --- a/src/entari_plugin_llm/__init__.py +++ b/src/entari_plugin_llm/__init__.py @@ -1,4 +1,4 @@ -from arclet.entari import declare_static, metadata +from arclet.entari import declare_static, metadata from .config import Config from .events import LLMToolEvent as LLMToolEvent @@ -15,4 +15,10 @@ _suppress_litellm_logging() from . import listeners as listeners +from . import matchers as matchers from .service import llm as llm + +__all__ = [ + "llm", + "LLMToolEvent", +] diff --git a/src/entari_plugin_llm/callback.py b/src/entari_plugin_llm/_callback.py similarity index 100% rename from src/entari_plugin_llm/callback.py rename to src/entari_plugin_llm/_callback.py diff --git a/src/entari_plugin_llm/_handler.py b/src/entari_plugin_llm/_handler.py new file mode 100644 index 0000000..689ed92 --- /dev/null +++ b/src/entari_plugin_llm/_handler.py @@ -0,0 +1,281 @@ +import json +from collections.abc import Sequence +from typing import Any, overload +from uuid import uuid4 + +import litellm +from arclet.entari import Session, User +from arclet.letoderea import ExitState +from arclet.letoderea.typing import Contexts, generate_contexts +from entari_plugin_database import get_session as get_db_session +from sqlalchemy import desc, func, select + +from ._types import Message +from .events.tools import LLMToolEvent, available_functions, tools +from .log import logger +from .model import LLMSession, SessionContext +from .service import llm + + +class LLMSessionManager: + @classmethod + async def _generate_topic(cls, user_input: str, model: str | None = None) -> str: + prompt = ( + "请根据用户的这句输入,生成一个简短的话题标题。" + "只输出标题,不要解释,限制在12个字以内。\n" + f"用户输入:{user_input}" + ) + try: + result = await llm.generate(prompt, stream=False, model=model) + topic = (result.choices[0]["message"]["content"] or "").strip() + return topic or "新对话" + except Exception: + return "新对话" + + @classmethod + async def _get_active_session(cls, user_id: str) -> LLMSession | None: + async with get_db_session() as db_session: + stmt = ( + select(LLMSession) + .where(LLMSession.user_id == user_id, LLMSession.is_active.is_(True)) + .order_by(desc(LLMSession.created_at)) + .limit(1) + ) + return await db_session.scalar(stmt) + + @overload + @classmethod + async def _create_session(cls, user_id: str, *, topic: str, model: str | None = None) -> LLMSession: ... + + @overload + @classmethod + async def _create_session(cls, user_id: str, *, user_input: str, model: str | None = None) -> LLMSession: ... + + @classmethod + async def _create_session( + cls, + user_id: str, + user_input: str | None = None, + topic: str | None = None, + model: str | None = None, + ) -> LLMSession: + if topic is None and user_input: + topic = await cls._generate_topic(user_input=user_input, model=model) + + user_session = LLMSession(session_id=uuid4().hex, user_id=user_id, topic=topic, is_active=True) + async with get_db_session() as db_session: + active_stmt = select(LLMSession).where(LLMSession.user_id == user_id, LLMSession.is_active.is_(True)) + active_sessions = (await db_session.scalars(active_stmt)).all() + for active in active_sessions: + active.is_active = False + db_session.add(user_session) + await db_session.commit() + return user_session + + @classmethod + async def _load_messages(cls, user_id: str) -> list[Message]: + async with get_db_session() as db_session: + stmt = ( + select(SessionContext) + .where(SessionContext.session_id == user_id) + .order_by(SessionContext.id.asc()) + ) + contexts = list((await db_session.scalars(stmt)).all()) + return [context.message for context in contexts] + + @classmethod + async def _persist_message(cls, session_id: str, message: Message) -> None: + async with get_db_session() as db_session: + db_session.add( + SessionContext( + session_id=session_id, + role=message["role"], + content=message["content"], + reasoning_content=message.get("reasoning_content"), + name=message.get("name"), + tool_calls=message.get("tool_calls"), + tool_call_id=message.get("tool_call_id"), + ) + ) + await db_session.commit() + + @classmethod + async def _add_token_usage(cls, session_id: str, tokens: int) -> None: + if tokens <= 0: + return + + async with get_db_session() as db_session: + user_session = await db_session.get(LLMSession, session_id) + if user_session is None: + return + user_session.total_tokens += tokens + await db_session.commit() + + @classmethod + async def _refresh_topic( + cls, + llm_session: LLMSession, + user_input: str, + model: str | None = None, + ) -> None: + async with get_db_session() as db_session: + user_session = await db_session.get(LLMSession, llm_session.session_id) + if user_session is None: + return + user_session.topic = await cls._generate_topic(user_input=user_input, model=model) + await db_session.commit() + llm_session.topic = user_session.topic + + @classmethod + async def create_new_session(cls, user: User) -> LLMSession: + return await cls._create_session(user_id=user.id, topic="新对话") + + @classmethod + async def switch(cls, user: User, session_id: str) -> bool: + async with get_db_session() as db_session: + target = await db_session.get(LLMSession, session_id) + if target is None or target.user_id != user.id: + return False + + if target.is_active: + return True + + active_stmt = select(LLMSession).where(LLMSession.user_id == user.id, LLMSession.is_active.is_(True)) + active_sessions = (await db_session.scalars(active_stmt)).all() + for active in active_sessions: + active.is_active = False + target.is_active = True + await db_session.commit() + return True + + @classmethod + async def delete(cls, user: User, session_id: str) -> bool: + async with get_db_session() as db_session: + user_session = await db_session.get(LLMSession, session_id) + if user_session is None or user_session.user_id != user.id: + return False + await db_session.delete(user_session) + await db_session.commit() + return True + + @classmethod + async def get_current_session_info(cls, user: User) -> dict[str, Any] | None: + async with get_db_session() as db_session: + stmt = ( + select(LLMSession) + .where(LLMSession.user_id == user.id, LLMSession.is_active.is_(True)) + .order_by(desc(LLMSession.created_at)) + .limit(1) + ) + session = await db_session.scalar(stmt) + if session is None: + return None + + count_stmt = ( + select(func.count(SessionContext.id)) + .where(SessionContext.session_id == session.session_id) + .where(SessionContext.role.in_(("user", "assistant"))) + ) + message_count = int(await db_session.scalar(count_stmt) or 0) + + return { + "session_id": session.session_id, + "topic": session.topic, + "is_active": session.is_active, + "created_at": session.created_at, + "message_count": message_count, + "total_tokens": session.total_tokens, + } + + @classmethod + async def list_sessions(cls, user: User) -> Sequence[LLMSession]: + async with get_db_session() as db_session: + stmt = select(LLMSession).where(LLMSession.user_id == user.id).order_by(desc(LLMSession.created_at)) + return list((await db_session.scalars(stmt)).all()) + + @classmethod + async def chat( + cls, + user_input: str, + ctx: Contexts, + session: Session, + model: str | None = None, + new: bool = False, + ) -> str: + llm_session = await cls._get_active_session(session.user.id) + if new or llm_session is None: + llm_session = await cls._create_session(user_id=session.user.id, user_input=user_input, model=model) + + if llm_session.topic == "新对话": + await cls._refresh_topic(llm_session, user_input=user_input, model=model) + + user_message: Message = {"role": "user", "content": user_input, "name": session.user.name} + await cls._persist_message(llm_session.session_id, user_message) + + messages = await cls._load_messages(llm_session.session_id) + final_answer = "" + for _ in range(8): + response = await llm.generate( + messages, + stream=False, + model=model, + tools=tools, + tool_choice="auto", + user=session.user.name, + ) + + usage = response.get("usage") or {} + await cls._add_token_usage( + llm_session.session_id, + int(usage.get("total_tokens", 0) or 0), + ) + + response_message = response["choices"][0]["message"] + tool_calls = response_message.tool_calls + + assistant_message: Message = { + "role": "assistant", + "content": response_message.content, + "tool_calls": [tc.model_dump() for tc in tool_calls] if tool_calls else None, + } + messages.append(assistant_message) + await cls._persist_message(llm_session.session_id, assistant_message) + + if tool_calls: + calls = [tc for tc in tool_calls if isinstance(tc, litellm.ChatCompletionMessageToolCall)] + for tool_call in calls: + function_name = tool_call.function.name + if function_name is None: + continue + + function_to_call = available_functions[function_name] + function_args = json.loads(tool_call.function.arguments) + ctx1 = await generate_contexts(LLMToolEvent(), inherit_ctx=ctx) + logger.info(f"Calling tool: {function_name} with args: {function_args}") + try: + resp = await function_to_call.handle(ctx1 | function_args, inner=True) + if isinstance(resp, ExitState): + if resp.args[0] is not None: + result: dict[str, Any] = {"ok": True, "data": resp.args[0]} + else: + result = {"ok": False, "error": "No response"} + else: + result = {"ok": True, "data": resp} + except Exception as e: + result = {"ok": False, "error": repr(e)} + + tool_message: Message = { + "tool_call_id": tool_call.id, + "role": "tool", + "content": json.dumps(result, ensure_ascii=False), + } + messages.append(tool_message) + await cls._persist_message(llm_session.session_id, tool_message) + continue + + final_answer = response_message.content or "" + break + + if not final_answer: + return "对话失败,请稍后再试" + return final_answer diff --git a/src/entari_plugin_llm/_jsondata.py b/src/entari_plugin_llm/_jsondata.py new file mode 100644 index 0000000..20baa70 --- /dev/null +++ b/src/entari_plugin_llm/_jsondata.py @@ -0,0 +1,54 @@ +import json +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any + +from arclet.entari import local_data + + +@dataclass(slots=True) +class LLMState: + default_model: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "LLMState": + value = data.get("default_model") + default_model = value if isinstance(value, str) and value else None + return cls(default_model=default_model) + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +def _state_path() -> Path: + return local_data.get_data_file("entari_plugin_llm", "state.json") + + +def _read_state() -> LLMState: + path = _state_path() + if not path.exists(): + return LLMState() + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return LLMState() + if not isinstance(data, dict): + return LLMState() + return LLMState.from_dict(data) + + +def _write_state(data: LLMState) -> None: + path = _state_path() + path.write_text( + json.dumps(data.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8" + ) + + +def get_default_model() -> str | None: + return _read_state().default_model + + +def set_default_model(model_name: str | None) -> None: + state = _read_state() + state.default_model = model_name if model_name else None + _write_state(state) diff --git a/src/entari_plugin_llm/config.py b/src/entari_plugin_llm/config.py index cdeb196..7205891 100644 --- a/src/entari_plugin_llm/config.py +++ b/src/entari_plugin_llm/config.py @@ -3,6 +3,9 @@ from arclet.entari import BasicConfModel, plugin_config from arclet.entari.config import model_field +from ._jsondata import get_default_model +from .exception import ModelNotFoundError + class ScopedModel(BasicConfModel): name: str @@ -36,14 +39,22 @@ class Config(BasicConfModel): def get_model_config(model_name: str | None = None) -> ScopedModel: if model_name is None: if not _conf.models: - raise ValueError("No models configured.") - model_name = _conf.models[0].name + raise ModelNotFoundError("No models configured.") + + model_name = get_default_model() for model in _conf.models: if model.name == model_name or model.alias == model_name: if model.api_key is None: model.api_key = _conf.api_key - if model.base_url == "https://api.openai.com/v1" and _conf.base_url != "https://api.openai.com/v1": + if ( + model.base_url == "https://api.openai.com/v1" + and _conf.base_url != "https://api.openai.com/v1" + ): model.base_url = _conf.base_url return model - raise ValueError(f"Model {model_name} not found in config.") + raise ModelNotFoundError(f"Model {model_name} not found in config.") + + +def get_model_list() -> set[str]: + return {m.name for m in _conf.models} | {m.alias for m in _conf.models if m.alias} diff --git a/src/entari_plugin_llm/exception.py b/src/entari_plugin_llm/exception.py new file mode 100644 index 0000000..911d2ab --- /dev/null +++ b/src/entari_plugin_llm/exception.py @@ -0,0 +1,2 @@ +class ModelNotFoundError(Exception): + ... diff --git a/src/entari_plugin_llm/listeners/__init__.py b/src/entari_plugin_llm/listeners/__init__.py index f1e7177..17abe1c 100644 --- a/src/entari_plugin_llm/listeners/__init__.py +++ b/src/entari_plugin_llm/listeners/__init__.py @@ -1,22 +1,55 @@ -import json -from collections import deque +from collections import deque -import litellm from arclet.entari import MessageCreatedEvent, Session, filter_ from arclet.entari.config import config_model_validate from arclet.entari.event.config import ConfigReload +from arclet.entari.event.lifespan import Ready from arclet.entari.event.send import SendResponse -from arclet.letoderea import BLOCK, ExitState, on -from arclet.letoderea.typing import Contexts, generate_contexts +from arclet.letoderea import BLOCK, on +from arclet.letoderea.typing import Contexts +from .._handler import LLMSessionManager +from .._jsondata import get_default_model, set_default_model from ..config import Config, _conf -from ..events.tools import LLMToolEvent, available_functions, tools from ..log import logger -from ..service import llm RECORD = deque(maxlen=16) +@on(Ready) +async def _(): + if not _conf.models: + set_default_model(None) + logger.warning("未配置任何模型,已清空本地默认模型配置") + return + + first_model = _conf.models[0].name + default_model = get_default_model() + if not default_model: + set_default_model(first_model) + logger.info(f"未检测到本地默认模型,已设置为首个模型: {first_model}") + return + + matched = next( + ( + m + for m in _conf.models + if m.name == default_model or m.alias == default_model + ), + None, + ) + if matched is None: + set_default_model(first_model) + logger.warning( + f"本地默认模型不存在于当前配置: {default_model},已重置为: {first_model}", + ) + return + + if matched.name != default_model: + set_default_model(matched.name) + logger.info(f"已将本地默认模型标准化为模型名: {matched.name}") + + @on(SendResponse) async def _record(event: SendResponse): if event.result and event.session: @@ -27,65 +60,10 @@ async def _record(event: SendResponse): async def run_conversation(session: Session, ctx: Contexts): if session.event.sn in RECORD: return BLOCK - msg = session.elements.extract_plain_text() - messages: list = [{"role": "user", "content": msg, "name": session.user.name}] - final_answer = "" - for step in range(8): - response = await llm.generate( - messages, - stream=False, - tools=tools, - tool_choice="auto", - ) - - response_message = response["choices"][0]["message"] - tool_calls = response_message.tool_calls - messages.append( - { - "role": "assistant", - "content": response_message.content, - "tool_calls": [tc.model_dump() for tc in tool_calls] if tool_calls else None, - } - ) - - if tool_calls: - calls = [tc for tc in tool_calls if isinstance(tc, litellm.ChatCompletionMessageToolCall)] - for tool_call in calls: - function_name = tool_call.function.name - if function_name is None: - continue - - function_to_call = available_functions[function_name] - function_args = json.loads(tool_call.function.arguments) - ctx1 = await generate_contexts(LLMToolEvent(), inherit_ctx=ctx) - logger.info(f"Calling tool: {function_name} with args: {function_args}") - try: - resp = await function_to_call.handle(ctx1 | function_args, inner=True) - if isinstance(resp, ExitState): - if resp.args[0] is not None: - result = {"ok": True, "data": resp.args[0]} - else: - result = {"ok": False, "error": "No response"} - else: - result = {"ok": True, "data": resp} - except Exception as e: - result = {"ok": False, "error": repr(e)} - messages.append( - { - "tool_call_id": tool_call.id, - "role": "tool", - "name": function_name, - "content": json.dumps(result, ensure_ascii=False), - } - ) - continue - final_answer = response_message.content or "" - break - if final_answer: - await session.send(final_answer) - else: - await session.send("对话失败,请稍后再试") + msg = session.elements.extract_plain_text() + answer = await LLMSessionManager.chat(user_input=msg, ctx=ctx, session=session) + await session.send(answer) return BLOCK diff --git a/src/entari_plugin_llm/matchers/__init__.py b/src/entari_plugin_llm/matchers/__init__.py new file mode 100644 index 0000000..4b703f8 --- /dev/null +++ b/src/entari_plugin_llm/matchers/__init__.py @@ -0,0 +1,176 @@ +from arclet.alconna import Alconna, Args, MultiVar, Option, Subcommand, store_true +from arclet.entari import Reply, Session, command +from arclet.letoderea import BLOCK, Contexts + +from .._handler import LLMSessionManager +from .._jsondata import set_default_model +from ..config import get_model_config, get_model_list +from ..exception import ModelNotFoundError +from .utils import render_model_list, render_session_list, select_session + +llm_alc = Alconna( + "llm", + Args["content?#内容", MultiVar(str)], + Option("-m|--model", Args["model?#模型名称", str], help_text="指定模型"), + Option( + "-n|--new", + dest="new_opt", + default=False, + action=store_true, + help_text="创建新会话", + ), + Subcommand("new", dest="new_cmd", help_text="创建新会话"), + Subcommand("switch", Args["session_id?#会话ID", str], help_text="切换会话"), + Subcommand("delete", Args["session_id?#会话ID", str], help_text="删除会话"), + Subcommand( + "session", + Option("-l|--list", help_text="查看会话列表"), + help_text="查看当前会话信息", + ), + Subcommand( + "model", + Args["model?#模型名称", str], + Option("-l|--list", help_text="查看模型列表"), + help_text="查看当前模型信息", + ), +) + +llm_alc.shortcut("ai", {"command": "llm", "fuzzy": True, "prefix": True}) + +llm_disp = command.mount(llm_alc) + + +@llm_disp.handle(priority=25) +async def _( + ctx: Contexts, + session: Session, + content: command.Match[tuple[str, ...]], + new_opt: command.Query[bool] = command.Query("new_opt.value"), + model: command.Query[str] = command.Query("model.model"), +): + reply: Reply | None = ctx.get("$message_reply") + + user_input = " ".join(content.result) if content.available else "" + + if reply: + user_input = f"{user_input} {reply.origin.content}".strip() + + if not user_input: + resp = await session.prompt("需要我为你做些什么?") + if not resp: + await session.send("等待超时") + return BLOCK + user_input = resp.extract_plain_text() + + try: + answer = await LLMSessionManager.chat( + user_input=user_input, + ctx=ctx, + session=session, + model=model.result if model.available else None, + new=new_opt.result, + ) + await session.send(answer) + except ModelNotFoundError as e: + await session.send(str(e)) + except Exception as e: + await session.send(str(e)) + + return BLOCK + + +@llm_disp.assign("new_cmd") +async def _(session: Session): + new_session = await LLMSessionManager.create_new_session(session.user) + await session.send(f"以创建并切换到新会话\n会话ID: {new_session.session_id}") + return BLOCK + + +@llm_disp.assign("switch") +async def _(session: Session, session_id: command.Match[str]): + if not session_id.available: + selected = await select_session(session) + if selected is None: + return BLOCK + + session_id.result = selected + + switched = await LLMSessionManager.switch(session.user, session_id.result) + await session.send("切换成功" if switched else "未找到对应会话") + return BLOCK + + +@llm_disp.assign("delete") +async def _(session: Session, session_id: command.Match[str]): + if not session_id.available: + selected = await select_session(session) + if selected is None: + return BLOCK + + session_id.result = selected + + deleted = await LLMSessionManager.delete(session.user, session_id.result) + if deleted: + await LLMSessionManager.create_new_session(session.user) + await session.send("删除成功,已自动创建新会话") + else: + await session.send("未找到对应会话") + return BLOCK + + +@llm_disp.assign("session", priority=20) +async def _(session: Session): + info = await LLMSessionManager.get_current_session_info(session.user) + if info is None: + await session.send("当前没有活动会话") + return BLOCK + + created_at = info["created_at"].strftime("%Y-%m-%d %H:%M:%S") + await session.send( + "\n".join( + [ + f"会话ID: {info['session_id']}", + f"话题: {info['topic']}", + f"消息数: {info['message_count']}", + f"累计 Token: {info['total_tokens']}", + f"创建时间: {created_at}", + ] + ) + ) + return BLOCK + + +@llm_disp.assign("session.list") +async def _(session: Session): + rows = await LLMSessionManager.list_sessions(session.user) + + if not rows: + await session.send("暂无会话") + return BLOCK + + await session.send(render_session_list(rows)) + return BLOCK + + +@llm_disp.assign("model", priority=20) +async def _(session: Session, model: command.Match[str]): + if model.available: + if model.result not in get_model_list(): + await session.send(render_model_list()) + return BLOCK + + conf = get_model_config(model.result) + set_default_model(conf.name) + + await session.send(f"已切换默认模型: {conf.name}") + return BLOCK + + conf = get_model_config() + await session.send(render_model_list()) + return BLOCK + + +@llm_disp.assign("model.list") +async def _(session: Session): + await session.send(render_model_list()) + return BLOCK diff --git a/src/entari_plugin_llm/matchers/utils.py b/src/entari_plugin_llm/matchers/utils.py new file mode 100644 index 0000000..84f0f75 --- /dev/null +++ b/src/entari_plugin_llm/matchers/utils.py @@ -0,0 +1,60 @@ +from collections.abc import Sequence + +from arclet.entari import Session + +from .._handler import LLMSessionManager +from .._jsondata import get_default_model +from ..config import _conf +from ..model import LLMSession + + +def _parse_session_id(choice: str, rows: Sequence[LLMSession]) -> str | None: + text = choice.strip() + if not text: + return None + if text.isdigit(): + index = int(text) + if 1 <= index <= len(rows): + return rows[index - 1].session_id + return None + for row in rows: + if row.session_id == text: + return row.session_id + return None + + +def render_session_list(rows: Sequence[LLMSession]) -> str: + lines = [f"会话列表(共 {len(rows)} 个)"] + for idx, row in enumerate(rows, 1): + flag = " [当前]" if row.is_active else "" + lines.append(f"{idx}. {row.topic}{flag} | ID: {row.session_id}") + return "\n".join(lines) + + +def render_model_list() -> str: + default_model = get_default_model() + lines = [f"模型列表(共 {len(_conf.models)} 个)"] + for model in _conf.models: + alias = f" ({model.alias})" if model.alias else "" + is_default = " [默认]" if default_model == model.name else "" + lines.append(f"- {model.name}{alias}{is_default}") + return "\n".join(lines) + + +async def select_session(session: Session) -> str | None: + rows = await LLMSessionManager.list_sessions(session.user) + if not rows: + await session.send("暂无会话") + return None + + prompt_text = f"{render_session_list(rows)}\n请输入会话序号或ID:" + resp = await session.prompt(prompt_text) + if resp is None: + await session.send("等待超时") + return None + + selected = _parse_session_id(resp.extract_plain_text(), rows) + if selected is None: + await session.send("输入无效,请输入会话序号或ID") + return None + return selected diff --git a/src/entari_plugin_llm/model.py b/src/entari_plugin_llm/model.py new file mode 100644 index 0000000..43e74d9 --- /dev/null +++ b/src/entari_plugin_llm/model.py @@ -0,0 +1,66 @@ +import json +from datetime import datetime +from typing import Literal, TypeAlias, cast + +from entari_plugin_database import Base +from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, Text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from ._types import Message + +ROLE: TypeAlias = Literal["user", "assistant", "tool"] + + +class LLMSession(Base): + __tablename__ = "entari_plugin_llm_session" + + session_id: Mapped[str] = mapped_column(String(64), primary_key=True) + user_id: Mapped[str] = mapped_column(String(64), index=True) + topic: Mapped[str] = mapped_column(String(24)) + is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True) + + total_tokens: Mapped[int] = mapped_column(default=0) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + + contexts = relationship("SessionContext", back_populates="session", cascade="all, delete-orphan") + + +class SessionContext(Base): + __tablename__ = "entari_plugin_llm_context" + + id: Mapped[int] = mapped_column(primary_key=True) + session_id: Mapped[str] = mapped_column(String(64), ForeignKey("entari_plugin_llm_session.session_id"), index=True) + + role: Mapped[ROLE] = mapped_column(String(16)) + content: Mapped[str] = mapped_column(Text) + reasoning_content: Mapped[str | None] = mapped_column(Text, nullable=True) + name: Mapped[str | None] = mapped_column(String(64), nullable=True) + + tool_calls: Mapped[list | None] = mapped_column(JSON, nullable=True) + tool_call_id: Mapped[str | None] = mapped_column(String(64), nullable=True) + + session = relationship("LLMSession", back_populates="contexts") + + @property + def message(self) -> Message: + msg = {"role": self.role, "content": self.content} + + if self.role == "user": + if self.name: + msg["name"] = self.name + + elif self.role == "assistant": + if self.reasoning_content: + msg["reasoning_content"] = self.reasoning_content + if self.tool_calls: + msg["tool_calls"] = json.dumps(self.tool_calls) + msg["content"] = self.content + + elif self.role == "tool" and self.tool_call_id: + msg["tool_call_id"] = self.tool_call_id + + elif self.role == "system": + if self.name: + msg["name"] = self.name + + return cast(Message, msg) diff --git a/src/entari_plugin_llm/service.py b/src/entari_plugin_llm/service.py index fc87342..9915070 100644 --- a/src/entari_plugin_llm/service.py +++ b/src/entari_plugin_llm/service.py @@ -6,8 +6,8 @@ from launart import Launart, Service from launart.status import Phase +from ._callback import TokenUsageHandler from ._types import Message -from .callback import TokenUsageHandler from .config import get_model_config from .log import log From 2c5009dec8dc5e0d6793cd3f48d879b30961c1a9 Mon Sep 17 00:00:00 2001 From: KomoriDev Date: Sat, 21 Feb 2026 21:55:41 +0800 Subject: [PATCH 2/3] :truck: modify the structure --- src/entari_plugin_llm/__init__.py | 3 +- .../__init__.py => handlers/chat.py} | 40 +----------------- src/entari_plugin_llm/handlers/check.py | 41 +++++++++++++++++++ .../__init__.py => handlers/command.py} | 3 +- .../{_handler.py => handlers/manager.py} | 10 ++--- .../{matchers => handlers}/utils.py | 2 +- 6 files changed, 51 insertions(+), 48 deletions(-) rename src/entari_plugin_llm/{listeners/__init__.py => handlers/chat.py} (50%) create mode 100644 src/entari_plugin_llm/handlers/check.py rename src/entari_plugin_llm/{matchers/__init__.py => handlers/command.py} (98%) rename src/entari_plugin_llm/{_handler.py => handlers/manager.py} (98%) rename src/entari_plugin_llm/{matchers => handlers}/utils.py (97%) diff --git a/src/entari_plugin_llm/__init__.py b/src/entari_plugin_llm/__init__.py index b9d455d..1de0f3c 100644 --- a/src/entari_plugin_llm/__init__.py +++ b/src/entari_plugin_llm/__init__.py @@ -14,8 +14,7 @@ declare_static() _suppress_litellm_logging() -from . import listeners as listeners -from . import matchers as matchers +from .handlers import command as command from .service import llm as llm __all__ = [ diff --git a/src/entari_plugin_llm/listeners/__init__.py b/src/entari_plugin_llm/handlers/chat.py similarity index 50% rename from src/entari_plugin_llm/listeners/__init__.py rename to src/entari_plugin_llm/handlers/chat.py index 17abe1c..e456bdd 100644 --- a/src/entari_plugin_llm/listeners/__init__.py +++ b/src/entari_plugin_llm/handlers/chat.py @@ -3,53 +3,15 @@ from arclet.entari import MessageCreatedEvent, Session, filter_ from arclet.entari.config import config_model_validate from arclet.entari.event.config import ConfigReload -from arclet.entari.event.lifespan import Ready from arclet.entari.event.send import SendResponse from arclet.letoderea import BLOCK, on from arclet.letoderea.typing import Contexts -from .._handler import LLMSessionManager -from .._jsondata import get_default_model, set_default_model from ..config import Config, _conf -from ..log import logger +from .manager import LLMSessionManager RECORD = deque(maxlen=16) - -@on(Ready) -async def _(): - if not _conf.models: - set_default_model(None) - logger.warning("未配置任何模型,已清空本地默认模型配置") - return - - first_model = _conf.models[0].name - default_model = get_default_model() - if not default_model: - set_default_model(first_model) - logger.info(f"未检测到本地默认模型,已设置为首个模型: {first_model}") - return - - matched = next( - ( - m - for m in _conf.models - if m.name == default_model or m.alias == default_model - ), - None, - ) - if matched is None: - set_default_model(first_model) - logger.warning( - f"本地默认模型不存在于当前配置: {default_model},已重置为: {first_model}", - ) - return - - if matched.name != default_model: - set_default_model(matched.name) - logger.info(f"已将本地默认模型标准化为模型名: {matched.name}") - - @on(SendResponse) async def _record(event: SendResponse): if event.result and event.session: diff --git a/src/entari_plugin_llm/handlers/check.py b/src/entari_plugin_llm/handlers/check.py new file mode 100644 index 0000000..c610437 --- /dev/null +++ b/src/entari_plugin_llm/handlers/check.py @@ -0,0 +1,41 @@ +from arclet.entari.event.lifespan import Ready +from arclet.letoderea import on + +from .._jsondata import get_default_model, set_default_model +from ..config import _conf +from ..log import logger + + +@on(Ready) +async def _(): + if not _conf.models: + set_default_model(None) + logger.warning("未配置任何模型,已清空本地默认模型配置") + return + + first_model = _conf.models[0].name + default_model = get_default_model() + if not default_model: + set_default_model(first_model) + logger.info(f"未检测到本地默认模型,已设置为首个模型: {first_model}") + return + + matched = next( + ( + m + for m in _conf.models + if m.name == default_model or m.alias == default_model + ), + None, + ) + if matched is None: + set_default_model(first_model) + logger.warning( + f"本地默认模型不存在于当前配置: {default_model},已重置为: {first_model}", + ) + return + + if matched.name != default_model: + set_default_model(matched.name) + logger.info(f"已将本地默认模型标准化为模型名: {matched.name}") + diff --git a/src/entari_plugin_llm/matchers/__init__.py b/src/entari_plugin_llm/handlers/command.py similarity index 98% rename from src/entari_plugin_llm/matchers/__init__.py rename to src/entari_plugin_llm/handlers/command.py index 4b703f8..428bdaf 100644 --- a/src/entari_plugin_llm/matchers/__init__.py +++ b/src/entari_plugin_llm/handlers/command.py @@ -2,10 +2,11 @@ from arclet.entari import Reply, Session, command from arclet.letoderea import BLOCK, Contexts -from .._handler import LLMSessionManager from .._jsondata import set_default_model from ..config import get_model_config, get_model_list from ..exception import ModelNotFoundError +from . import chat as chat +from .manager import LLMSessionManager from .utils import render_model_list, render_session_list, select_session llm_alc = Alconna( diff --git a/src/entari_plugin_llm/_handler.py b/src/entari_plugin_llm/handlers/manager.py similarity index 98% rename from src/entari_plugin_llm/_handler.py rename to src/entari_plugin_llm/handlers/manager.py index 689ed92..c9002f0 100644 --- a/src/entari_plugin_llm/_handler.py +++ b/src/entari_plugin_llm/handlers/manager.py @@ -10,11 +10,11 @@ from entari_plugin_database import get_session as get_db_session from sqlalchemy import desc, func, select -from ._types import Message -from .events.tools import LLMToolEvent, available_functions, tools -from .log import logger -from .model import LLMSession, SessionContext -from .service import llm +from .._types import Message +from ..events.tools import LLMToolEvent, available_functions, tools +from ..log import logger +from ..model import LLMSession, SessionContext +from ..service import llm class LLMSessionManager: diff --git a/src/entari_plugin_llm/matchers/utils.py b/src/entari_plugin_llm/handlers/utils.py similarity index 97% rename from src/entari_plugin_llm/matchers/utils.py rename to src/entari_plugin_llm/handlers/utils.py index 84f0f75..d0b5701 100644 --- a/src/entari_plugin_llm/matchers/utils.py +++ b/src/entari_plugin_llm/handlers/utils.py @@ -2,7 +2,7 @@ from arclet.entari import Session -from .._handler import LLMSessionManager +from .manager import LLMSessionManager from .._jsondata import get_default_model from ..config import _conf from ..model import LLMSession From 4853c8aed5f2e64e26584e31cf30741d89b4cd7f Mon Sep 17 00:00:00 2001 From: KomoriDev Date: Sat, 21 Feb 2026 22:46:42 +0800 Subject: [PATCH 3/3] :truck: reorganize import: --- src/entari_plugin_llm/__init__.py | 2 ++ src/entari_plugin_llm/handlers/command.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/entari_plugin_llm/__init__.py b/src/entari_plugin_llm/__init__.py index 1de0f3c..d439712 100644 --- a/src/entari_plugin_llm/__init__.py +++ b/src/entari_plugin_llm/__init__.py @@ -14,6 +14,8 @@ declare_static() _suppress_litellm_logging() +from .handlers import chat as chat +from .handlers import check as check from .handlers import command as command from .service import llm as llm diff --git a/src/entari_plugin_llm/handlers/command.py b/src/entari_plugin_llm/handlers/command.py index 428bdaf..b6b61f4 100644 --- a/src/entari_plugin_llm/handlers/command.py +++ b/src/entari_plugin_llm/handlers/command.py @@ -5,7 +5,6 @@ from .._jsondata import set_default_model from ..config import get_model_config, get_model_list from ..exception import ModelNotFoundError -from . import chat as chat from .manager import LLMSessionManager from .utils import render_model_list, render_session_list, select_session