diff --git a/docker/requirements-full.txt b/docker/requirements-full.txt index be9ed2068..a14257a76 100644 --- a/docker/requirements-full.txt +++ b/docker/requirements-full.txt @@ -89,7 +89,7 @@ nvidia-cusparselt-cu12==0.6.3 nvidia-nccl-cu12==2.26.2 nvidia-nvjitlink-cu12==12.6.85 nvidia-nvtx-cu12==12.6.77 -ollama==0.4.9 +ollama==0.5.0 onnxruntime==1.22.1 openai==1.97.0 openapi-pydantic==0.5.1 @@ -184,3 +184,4 @@ py-key-value-aio==0.2.8 py-key-value-shared==0.2.8 PyJWT==2.10.1 pytest==9.0.2 +alibabacloud-oss-v2==1.2.2 diff --git a/docker/requirements.txt b/docker/requirements.txt index f89617c10..340f4e140 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -54,7 +54,7 @@ mdurl==0.1.2 more-itertools==10.8.0 neo4j==5.28.1 numpy==2.3.4 -ollama==0.4.9 +ollama==0.5.0 openai==1.109.1 openapi-pydantic==0.5.1 orjson==3.11.4 @@ -123,3 +123,4 @@ uvicorn==0.38.0 uvloop==0.22.1; sys_platform != 'win32' watchfiles==1.1.1 websockets==15.0.1 +alibabacloud-oss-v2==1.2.2 diff --git a/poetry.lock b/poetry.lock index fb818e665..ba31d1a31 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,23 @@ files = [ {file = "absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9"}, ] +[[package]] +name = "alibabacloud-oss-v2" +version = "1.2.2" +description = "Alibaba Cloud OSS (Object Storage Service) SDK V2 for Python" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"skill-mem\" or extra == \"all\"" +files = [ + {file = "alibabacloud_oss_v2-1.2.2-py3-none-any.whl", hash = "sha256:d138d1bdb38da6cc20d96b96faaeb099062a710a7f3d50f4b4b39a8cfcbdc120"}, +] + +[package.dependencies] +crcmod-plus = ">=2.1.0" +pycryptodome = ">=3.4.7" +requests = ">=2.18.4" + [[package]] name = "annotated-types" version = "0.7.0" @@ -582,6 +599,65 @@ mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.15.0)", " test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] +[[package]] +name = "crcmod-plus" +version = "2.3.1" +description = "CRC generator - modernized" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"skill-mem\" or extra == \"all\"" +files = [ + {file = "crcmod_plus-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:466d5fb9a05549a401164a2ba46a560779f7240f43f0b864e9fd277c5c12133a"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b31f039c440d59b808d1d90afbfd90ad901dc6e4a81d32a0fefa8d2c118064b9"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:24088832717435fc94d948e3140518c5a19fea99d1f6180b3396320398aca4c1"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5632576426e78c51ad4ed0569650e397f282cec2751862f3fd8a88dd9d5019a"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0313488db8e9048deee987f04859b9ad46c8e6fa26385fb1d3e481c771530961"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c1d8ae3ed019e9c164f1effee61cbc509ca39695738f7556fc0685e4c9218c86"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-win32.whl", hash = "sha256:bb54ac5623938726f4e92c18af0ccd9d119011e1821e949440bbfd24552ca539"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:79c58a3118e0c95cedffb48745fa1071982f8ba84309267b6020c2fffdbfaea7"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:b7e35e0f7d93d7571c2c9c3d6760e456999ea4c1eae5ead6acac247b5a79e469"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6853243120db84677b94b625112116f0ef69cd581741d20de58dce4c34242654"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:17735bc4e944d552ea18c8609fc6d08a5e64ee9b29cc216ba4d623754029cc3a"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac755040a2a35f43ab331978c48a9acb4ff64b425f282a296be467a410f00c3"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bdcfb838ca093ca673a3bbb37f62d1e5ec7182e00cc5ee2d00759f9f9f8ab11"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9166bc3c9b5e7b07b4e6854cac392b4a451b31d58d3950e48c140ab7b5d05394"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-win32.whl", hash = "sha256:cb99b694cce5c862560cf332a8b5e793620e28f0de3726995608bbd6f9b6e09a"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-win_amd64.whl", hash = "sha256:82b0f7e968c430c5a80fe0fc59e75cb54f2e84df2ed0cee5a3ff9cadfbf8a220"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fcb7a64648d70cac0a90c23bc6c58de6c13b28a0841c742039ba8528e23f51d1"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:abcf3ac30e41a58dd8d2659930e357d2fd47ab4fabb52382698ed1003c9a2598"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:693d2791af64aaf4467efe1473e02acd0ef1da229100262f29198f3ad59d42f8"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab075292b41b33be4d2f349e1139ea897023c3ebffc28c0d4c2ed7f2b31f1bce"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ccdc48e0af53c68304d60bbccfd5f51aed9979b5721016c3e097d51e0692b35e"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:283d23e4f13629413e6c963ffcc49c6166c9829b1e4ec6488e0d3703bd218dce"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:53319d2e9697a8d68260709aa61987fb89c49dd02b7f585b82c578659c1922b6"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c9ebd256f792ef01a1d0335419f679e7501d4fdf132a5206168c5269fcea65d0"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:52abc724f5232eddbe565c258878123337339bf9cfe9ac9c154e38557b8affc5"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b0e644395d68bbfb576ee28becb69d962b173fa648ce269aec260f538841fa9"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:07962695c53eedf3c9f0bacb2d7d6c00064394d4c88c0eb7d5b082808812fe82"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43acb79630192f91e60ec5b979a0e1fc2a4734182ce8b37d657f11fcd27c1f86"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:52aacdfc0f04510c9c0e6ecf7c09528543cb00f4d4edd0871be8c9b8e03f2c08"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac4ce5a423f3ccf143a42ce6af4661e2f806f09a6124c24996689b3457f1afcb"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-win32.whl", hash = "sha256:cf2df1058d6bf674c8b7b6f56c7ecdc0479707c81860f032abf69526f0111f70"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ba925ca53a1e00233a1b93380a46c0e821f6b797a19fc401aec85219cd85fd6f"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:22600072de422632531e92d7675faf223a5b2548d45c5cd6f77ec4575339900f"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f940704e359607b47b4a8e98c4d0f453f15bea039eb183cd0ffb14a8268fea78"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f939fc1f7d143962a8fbed2305ce5931627fea1ea3a7f1865c04dbba9d41bf67"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c6e8c7cf7ef49bcae7d3293996f82edde98e5fa202752ae58bf37a0289d35d"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:728f68d0e3049ba23978aaf277f3eb405dd21e78be6ba96382739ba09bba473c"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3829ed0dba48765f9b4139cb70b9bdf6553d2154302d9e3de6377556357892f"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-win32.whl", hash = "sha256:855fcbd07c3eb9162c701c1c7ed1a8b5a5f7b1e8c2dd3fd8ed2273e2f141ecc9"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:5422081be6403b6fba736c544e79c68410307f7a1a8ac1925b421a5c6f4591d3"}, + {file = "crcmod_plus-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9397324da1be2729f894744d9031a21ed97584c17fb0289e69e0c3c60916fc5f"}, + {file = "crcmod_plus-2.3.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:073c7a3b832652e66c41c8b8705eaecda704d1cbe850b9fa05fdee36cd50745a"}, + {file = "crcmod_plus-2.3.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e5f4c62553f772ea7ae12d9484801b752622c9c288e49ee7ea34a20b94e4920"}, + {file = "crcmod_plus-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5e80a9860f66f339956f540d86a768f4fe8c8bfcb139811f14be864425c48d64"}, + {file = "crcmod_plus-2.3.1.tar.gz", hash = "sha256:732ffe3c3ce3ef9b272e1827d8fb894590c4d6ff553f2a2b41ae30f4f94b0f5d"}, +] + +[package.extras] +dev = ["pytest"] + [[package]] name = "cryptography" version = "45.0.5" @@ -2853,14 +2929,14 @@ markers = {main = "platform_system == \"Linux\" and platform_machine == \"x86_64 [[package]] name = "ollama" -version = "0.4.9" +version = "0.5.0" description = "The official Python client for Ollama." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "ollama-0.4.9-py3-none-any.whl", hash = "sha256:18c8c85358c54d7f73d6a66cda495b0e3ba99fdb88f824ae470d740fbb211a50"}, - {file = "ollama-0.4.9.tar.gz", hash = "sha256:5266d4d29b5089a01489872b8e8f980f018bccbdd1082b3903448af1d5615ce7"}, + {file = "ollama-0.5.0-py3-none-any.whl", hash = "sha256:625371de663ccb48f14faa49bd85ae409da5e40d84cab42366371234b4dbaf68"}, + {file = "ollama-0.5.0.tar.gz", hash = "sha256:ed6a343b64de22f69309ac930d8ac12b46775aebe21cbb91b859b99f59c53fa7"}, ] [package.dependencies] @@ -3507,6 +3583,58 @@ files = [ ] markers = {main = "extra == \"mem-reader\" or extra == \"all\" or platform_python_implementation != \"PyPy\"", eval = "platform_python_implementation == \"PyPy\""} +[[package]] +name = "pycryptodome" +version = "3.23.0" +description = "Cryptographic library for Python" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "extra == \"skill-mem\" or extra == \"all\"" +files = [ + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -6234,14 +6362,15 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [extras] -all = ["cachetools", "chonkie", "datasketch", "jieba", "langchain-text-splitters", "markitdown", "neo4j", "nltk", "pika", "pymilvus", "pymysql", "qdrant-client", "rake-nltk", "rank-bm25", "redis", "schedule", "sentence-transformers", "torch", "volcengine-python-sdk"] +all = ["alibabacloud-oss-v2", "cachetools", "chonkie", "datasketch", "jieba", "langchain-text-splitters", "markitdown", "neo4j", "nltk", "pika", "pymilvus", "pymysql", "qdrant-client", "rake-nltk", "rank-bm25", "redis", "schedule", "sentence-transformers", "torch", "volcengine-python-sdk"] mem-reader = ["chonkie", "langchain-text-splitters", "markitdown"] mem-scheduler = ["pika", "redis"] mem-user = ["pymysql"] pref-mem = ["datasketch", "pymilvus"] +skill-mem = ["alibabacloud-oss-v2"] tree-mem = ["neo4j", "schedule"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "22bfcac5ed0be1e3aea294e3da96ff1a4bd9d7b62865ad827e1508f5ade6b708" +content-hash = "faff240c05a74263a404e8d9324ffd2f342cb4f0a4c1f5455b87349f6ccc61a5" diff --git a/pyproject.toml b/pyproject.toml index 3fbe4ced4..b010f4b50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ ] dependencies = [ "openai (>=1.77.0,<2.0.0)", - "ollama (>=0.4.8,<0.5.0)", + "ollama (>=0.5.0,<0.5.1)", "transformers (>=4.51.3,<5.0.0)", "tenacity (>=9.1.2,<10.0.0)", # Error handling and retrying library "fastapi[all] (>=0.115.12,<0.116.0)", # Web framework for building APIs @@ -97,6 +97,11 @@ pref-mem = [ "datasketch (>=1.6.5,<2.0.0)", # MinHash library ] +# SkillMemory +skill-mem = [ + "alibabacloud-oss-v2 (>=1.2.2,<1.2.3)", +] + # All optional dependencies # Allow users to install with `pip install MemoryOS[all]` all = [ @@ -123,6 +128,7 @@ all = [ "volcengine-python-sdk (>=4.0.4,<5.0.0)", "nltk (>=3.9.1,<4.0.0)", "rake-nltk (>=1.0.6,<1.1.0)", + "alibabacloud-oss-v2 (>=1.2.2,<1.2.3)", # Uncategorized dependencies ] diff --git a/src/memos/api/config.py b/src/memos/api/config.py index a3bf25be0..fb6e5e35e 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -467,6 +467,35 @@ def get_reader_config() -> dict[str, Any]: } @staticmethod + def get_oss_config() -> dict[str, Any] | None: + """Get OSS configuration and validate connection.""" + + config = { + "endpoint": os.getenv("OSS_ENDPOINT", "http://oss-cn-shanghai.aliyuncs.com"), + "access_key_id": os.getenv("OSS_ACCESS_KEY_ID", ""), + "access_key_secret": os.getenv("OSS_ACCESS_KEY_SECRET", ""), + "region": os.getenv("OSS_REGION", ""), + "bucket_name": os.getenv("OSS_BUCKET_NAME", ""), + } + + # Validate that all required fields have values + required_fields = [ + "endpoint", + "access_key_id", + "access_key_secret", + "region", + "bucket_name", + ] + missing_fields = [field for field in required_fields if not config.get(field)] + + if missing_fields: + logger.warning( + f"OSS configuration incomplete. Missing fields: {', '.join(missing_fields)}" + ) + return None + + return config + def get_internet_config() -> dict[str, Any]: """Get embedder configuration.""" reader_config = APIConfig.get_reader_config() @@ -746,6 +775,11 @@ def get_product_default_config() -> dict[str, Any]: ).split(",") if h.strip() ], + "oss_config": APIConfig.get_oss_config(), + "skills_dir_config": { + "skills_oss_dir": os.getenv("SKILLS_OSS_DIR", "skill_memory/"), + "skills_local_dir": os.getenv("SKILLS_LOCAL_DIR", "/tmp/skill_memory/"), + }, }, }, "enable_textual_memory": True, diff --git a/src/memos/api/handlers/component_init.py b/src/memos/api/handlers/component_init.py index bfbd6271d..008957bad 100644 --- a/src/memos/api/handlers/component_init.py +++ b/src/memos/api/handlers/component_init.py @@ -307,6 +307,9 @@ def init_server() -> dict[str, Any]: ) logger.debug("Searcher created") + # Set searcher to mem_reader + mem_reader.set_searcher(searcher) + # Initialize feedback server feedback_server = SimpleMemFeedback( llm=llm, diff --git a/src/memos/api/handlers/formatters_handler.py b/src/memos/api/handlers/formatters_handler.py index 6e1d9d1b6..cecc42c6c 100644 --- a/src/memos/api/handlers/formatters_handler.py +++ b/src/memos/api/handlers/formatters_handler.py @@ -112,13 +112,17 @@ def post_process_textual_mem( fact_mem = [ mem for mem in text_formatted_mem - if mem["metadata"]["memory_type"] not in ["ToolSchemaMemory", "ToolTrajectoryMemory"] + if mem["metadata"]["memory_type"] + in ["WorkingMemory", "LongTermMemory", "UserMemory", "OuterMemory"] ] tool_mem = [ mem for mem in text_formatted_mem if mem["metadata"]["memory_type"] in ["ToolSchemaMemory", "ToolTrajectoryMemory"] ] + skill_mem = [ + mem for mem in text_formatted_mem if mem["metadata"]["memory_type"] == "SkillMemory" + ] memories_result["text_mem"].append( { @@ -134,6 +138,13 @@ def post_process_textual_mem( "total_nodes": len(tool_mem), } ) + memories_result["skill_mem"].append( + { + "cube_id": mem_cube_id, + "memories": skill_mem, + "total_nodes": len(skill_mem), + } + ) return memories_result diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index d2aa2b204..978f5acdd 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -213,7 +213,7 @@ def handle_get_memory(memory_id: str, naive_mem_cube: NaiveMemCube) -> GetMemory def handle_get_memories( get_mem_req: GetMemoryRequest, naive_mem_cube: NaiveMemCube ) -> GetMemoryResponse: - results: dict[str, Any] = {"text_mem": [], "pref_mem": [], "tool_mem": []} + results: dict[str, Any] = {"text_mem": [], "pref_mem": [], "tool_mem": [], "skill_mem": []} memories = naive_mem_cube.text_mem.get_all( user_name=get_mem_req.mem_cube_id, user_id=get_mem_req.user_id, @@ -226,6 +226,8 @@ def handle_get_memories( if not get_mem_req.include_tool_memory: results["tool_mem"] = [] + if not get_mem_req.include_skill_memory: + results["skill_mem"] = [] preferences: list[TextualMemoryItem] = [] @@ -270,6 +272,7 @@ def handle_get_memories( "text_mem": results.get("text_mem", []), "pref_mem": results.get("pref_mem", []), "tool_mem": results.get("tool_mem", []), + "skill_mem": results.get("skill_mem", []), } return GetMemoryResponse(message="Memories retrieved successfully", data=filtered_results) diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index b2f8a9fa3..e6c4ae23d 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -358,6 +358,18 @@ class APISearchRequest(BaseRequest): description="Number of tool memories to retrieve (top-K). Default: 6.", ) + include_skill_memory: bool = Field( + True, + description="Whether to retrieve skill memories along with general memories. " + "If enabled, the system will automatically recall skill memories " + "relevant to the query. Default: True.", + ) + skill_mem_top_k: int = Field( + 3, + ge=0, + description="Number of skill memories to retrieve (top-K). Default: 3.", + ) + # ==== Filter conditions ==== # TODO: maybe add detailed description later filter: dict[str, Any] | None = Field( @@ -393,7 +405,7 @@ class APISearchRequest(BaseRequest): # Internal field for search memory type search_memory_type: str = Field( "All", - description="Type of memory to search: All, WorkingMemory, LongTermMemory, UserMemory, OuterMemory, ToolSchemaMemory, ToolTrajectoryMemory", + description="Type of memory to search: All, WorkingMemory, LongTermMemory, UserMemory, OuterMemory, ToolSchemaMemory, ToolTrajectoryMemory, SkillMemory", ) # ==== Context ==== @@ -772,7 +784,8 @@ class GetMemoryRequest(BaseRequest): mem_cube_id: str = Field(..., description="Cube ID") user_id: str | None = Field(None, description="User ID") include_preference: bool = Field(True, description="Whether to return preference memory") - include_tool_memory: bool = Field(False, description="Whether to return tool memory") + include_tool_memory: bool = Field(True, description="Whether to return tool memory") + include_skill_memory: bool = Field(True, description="Whether to return skill memory") filter: dict[str, Any] | None = Field(None, description="Filter for the memory") page: int | None = Field( None, diff --git a/src/memos/configs/mem_reader.py b/src/memos/configs/mem_reader.py index eaaa71461..4bd7953c0 100644 --- a/src/memos/configs/mem_reader.py +++ b/src/memos/configs/mem_reader.py @@ -57,6 +57,15 @@ class MultiModalStructMemReaderConfig(BaseMemReaderConfig): "If None, reads from FILE_PARSER_DIRECT_MARKDOWN_HOSTNAMES environment variable.", ) + oss_config: dict[str, Any] | None = Field( + default=None, + description="OSS configuration for the MemReader", + ) + skills_dir_config: dict[str, Any] | None = Field( + default=None, + description="Skills directory for the MemReader", + ) + class StrategyStructMemReaderConfig(BaseMemReaderConfig): """StrategyStruct MemReader configuration class.""" diff --git a/src/memos/mem_reader/base.py b/src/memos/mem_reader/base.py index 87bf43b0f..b034c9367 100644 --- a/src/memos/mem_reader/base.py +++ b/src/memos/mem_reader/base.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from memos.graph_dbs.base import BaseGraphDB + from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher class BaseMemReader(ABC): @@ -33,6 +34,12 @@ def set_graph_db(self, graph_db: "BaseGraphDB | None") -> None: graph_db: The graph database instance, or None to disable recall operations. """ + @abstractmethod + def set_searcher(self, searcher: "Searcher | None") -> None: + """ + Set the searcher instance for recall operations. + """ + @abstractmethod def get_memory( self, scene_data: list, type: str, info: dict[str, Any], mode: str = "fast" diff --git a/src/memos/mem_reader/factory.py b/src/memos/mem_reader/factory.py index 2749327bf..7bd551fb8 100644 --- a/src/memos/mem_reader/factory.py +++ b/src/memos/mem_reader/factory.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from memos.graph_dbs.base import BaseGraphDB + from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher class MemReaderFactory(BaseMemReader): @@ -27,6 +28,7 @@ def from_config( cls, config_factory: MemReaderConfigFactory, graph_db: Optional["BaseGraphDB | None"] = None, + searcher: Optional["Searcher | None"] = None, ) -> BaseMemReader: """ Create a MemReader instance from configuration. @@ -50,4 +52,7 @@ def from_config( if graph_db is not None: reader.set_graph_db(graph_db) + if searcher is not None: + reader.set_searcher(searcher) + return reader diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 9edcd0a55..352f25561 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -10,6 +10,7 @@ from memos.context.context import ContextThreadPoolExecutor from memos.mem_reader.read_multi_modal import MultiModalParser, detect_lang from memos.mem_reader.read_multi_modal.base import _derive_key +from memos.mem_reader.read_skill_memory.process_skill_memory import process_skill_memory_fine from memos.mem_reader.simple_struct import PROMPT_DICT, SimpleStructMemReader from memos.mem_reader.utils import parse_json_result from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata @@ -38,6 +39,12 @@ def __init__(self, config: MultiModalStructMemReaderConfig): # Extract direct_markdown_hostnames before converting to SimpleStructMemReaderConfig direct_markdown_hostnames = getattr(config, "direct_markdown_hostnames", None) + # oss + self.oss_config = getattr(config, "oss_config", None) + + # skills_dir + self.skills_dir_config = getattr(config, "skills_dir_config", None) + # Create config_dict excluding direct_markdown_hostnames for SimpleStructMemReaderConfig config_dict = config.model_dump(exclude_none=True) config_dict.pop("direct_markdown_hostnames", None) @@ -819,13 +826,27 @@ def _process_multi_modal_data( future_tool = executor.submit( self._process_tool_trajectory_fine, fast_memory_items, info, **kwargs ) + future_skill = executor.submit( + process_skill_memory_fine, + fast_memory_items=fast_memory_items, + info=info, + searcher=self.searcher, + graph_db=self.graph_db, + llm=self.llm, + embedder=self.embedder, + oss_config=self.oss_config, + skills_dir_config=self.skills_dir_config, + **kwargs, + ) # Collect results fine_memory_items_string_parser = future_string.result() fine_memory_items_tool_trajectory_parser = future_tool.result() + fine_memory_items_skill_memory_parser = future_skill.result() fine_memory_items.extend(fine_memory_items_string_parser) fine_memory_items.extend(fine_memory_items_tool_trajectory_parser) + fine_memory_items.extend(fine_memory_items_skill_memory_parser) # Part B: get fine multimodal items for fast_item in fast_memory_items: @@ -844,7 +865,7 @@ def _process_multi_modal_data( @timed def _process_transfer_multi_modal_data( - self, raw_node: TextualMemoryItem, custom_tags: list[str] | None = None, **kwargs + self, raw_nodes: list[TextualMemoryItem], custom_tags: list[str] | None = None, **kwargs ) -> list[TextualMemoryItem]: """ Process transfer for multimodal data. @@ -852,42 +873,56 @@ def _process_transfer_multi_modal_data( Each source is processed independently by its corresponding parser, which knows how to rebuild the original message and parse it in fine mode. """ - sources = raw_node.metadata.sources or [] - if not sources: - logger.warning("[MultiModalStruct] No sources found in raw_node") + if not raw_nodes: + logger.warning("[MultiModalStruct] No raw nodes found.") return [] - # Extract info from raw_node (same as simple_struct.py) + # Extract info from raw_nodes (same as simple_struct.py) info = { - "user_id": raw_node.metadata.user_id, - "session_id": raw_node.metadata.session_id, - **(raw_node.metadata.info or {}), + "user_id": raw_nodes[0].metadata.user_id, + "session_id": raw_nodes[0].metadata.session_id, + **(raw_nodes[0].metadata.info or {}), } fine_memory_items = [] # Part A: call llm in parallel using thread pool with ContextThreadPoolExecutor(max_workers=2) as executor: future_string = executor.submit( - self._process_string_fine, [raw_node], info, custom_tags, **kwargs + self._process_string_fine, raw_nodes, info, custom_tags, **kwargs ) future_tool = executor.submit( - self._process_tool_trajectory_fine, [raw_node], info, **kwargs + self._process_tool_trajectory_fine, raw_nodes, info, **kwargs + ) + future_skill = executor.submit( + process_skill_memory_fine, + raw_nodes, + info, + searcher=self.searcher, + llm=self.llm, + embedder=self.embedder, + graph_db=self.graph_db, + oss_config=self.oss_config, + skills_dir_config=self.skills_dir_config, + **kwargs, ) # Collect results fine_memory_items_string_parser = future_string.result() fine_memory_items_tool_trajectory_parser = future_tool.result() - + fine_memory_items_skill_memory_parser = future_skill.result() fine_memory_items.extend(fine_memory_items_string_parser) fine_memory_items.extend(fine_memory_items_tool_trajectory_parser) + fine_memory_items.extend(fine_memory_items_skill_memory_parser) # Part B: get fine multimodal items - for source in sources: - lang = getattr(source, "lang", "en") - items = self.multi_modal_parser.process_transfer( - source, context_items=[raw_node], info=info, custom_tags=custom_tags, lang=lang - ) - fine_memory_items.extend(items) + for raw_node in raw_nodes: + sources = raw_node.metadata.sources + for source in sources: + lang = getattr(source, "lang", "en") + items = self.multi_modal_parser.process_transfer( + source, context_items=[raw_node], info=info, custom_tags=custom_tags, lang=lang + ) + fine_memory_items.extend(items) return fine_memory_items def get_scene_data_info(self, scene_data: list, type: str) -> list[list[Any]]: @@ -944,22 +979,7 @@ def fine_transfer_simple_mem( if not input_memories: return [] - memory_list = [] - # Process Q&A pairs concurrently with context propagation - with ContextThreadPoolExecutor() as executor: - futures = [ - executor.submit( - self._process_transfer_multi_modal_data, scene_data_info, custom_tags, **kwargs - ) - for scene_data_info in input_memories - ] - for future in concurrent.futures.as_completed(futures): - try: - res_memory = future.result() - if res_memory is not None: - memory_list.append(res_memory) - except Exception as e: - logger.error(f"Task failed with exception: {e}") - logger.error(traceback.format_exc()) - return memory_list + memory_list = self._process_transfer_multi_modal_data(input_memories, custom_tags, **kwargs) + + return [memory_list] diff --git a/src/memos/mem_reader/read_skill_memory/process_skill_memory.py b/src/memos/mem_reader/read_skill_memory/process_skill_memory.py new file mode 100644 index 000000000..f341abc1c --- /dev/null +++ b/src/memos/mem_reader/read_skill_memory/process_skill_memory.py @@ -0,0 +1,650 @@ +import json +import os +import shutil +import uuid +import zipfile + +from concurrent.futures import as_completed +from datetime import datetime +from pathlib import Path +from typing import Any + +from memos.context.context import ContextThreadPoolExecutor +from memos.dependency import require_python_package +from memos.embedders.base import BaseEmbedder +from memos.graph_dbs.base import BaseGraphDB +from memos.llms.base import BaseLLM +from memos.log import get_logger +from memos.mem_reader.read_multi_modal import detect_lang +from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata +from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher +from memos.templates.skill_mem_prompt import ( + SKILL_MEMORY_EXTRACTION_PROMPT, + SKILL_MEMORY_EXTRACTION_PROMPT_ZH, + TASK_CHUNKING_PROMPT, + TASK_CHUNKING_PROMPT_ZH, + TASK_QUERY_REWRITE_PROMPT, + TASK_QUERY_REWRITE_PROMPT_ZH, +) +from memos.types import MessageList + + +logger = get_logger(__name__) + + +@require_python_package( + import_name="alibabacloud_oss_v2", + install_command="pip install alibabacloud-oss-v2", +) +def create_oss_client(oss_config: dict[str, Any] | None = None) -> Any: + import alibabacloud_oss_v2 as oss + + credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider() + + # load SDK's default configuration, and set credential provider + cfg = oss.config.load_default() + cfg.credentials_provider = credentials_provider + cfg.region = oss_config.get("region", os.getenv("OSS_REGION")) + cfg.endpoint = oss_config.get("endpoint", os.getenv("OSS_ENDPOINT")) + client = oss.Client(cfg) + + return client + + +def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem]) -> MessageList: + reconstructed_messages = [] + seen = set() # Track (role, content) tuples to detect duplicates + + for memory_item in memory_items: + for source_message in memory_item.metadata.sources: + try: + role = source_message.role + content = source_message.content + + # Create a tuple for deduplication + message_key = (role, content) + + # Only add if not seen before (keep first occurrence) + if message_key not in seen: + reconstructed_messages.append({"role": role, "content": content}) + seen.add(message_key) + except Exception as e: + logger.warning(f"Error reconstructing message: {e}") + continue + + return reconstructed_messages + + +def _add_index_to_message(messages: MessageList) -> MessageList: + for i, message in enumerate(messages): + message["idx"] = i + return messages + + +def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, MessageList]: + """Split messages into task chunks by LLM.""" + messages_context = "\n".join( + [ + f"{message.get('idx', i)}: {message['role']}: {message['content']}" + for i, message in enumerate(messages) + ] + ) + lang = detect_lang(messages_context) + template = TASK_CHUNKING_PROMPT_ZH if lang == "zh" else TASK_CHUNKING_PROMPT + prompt = [{"role": "user", "content": template.replace("{{messages}}", messages_context)}] + for attempt in range(3): + try: + response_text = llm.generate(prompt) + response_json = json.loads(response_text.replace("```json", "").replace("```", "")) + break + except Exception as e: + logger.warning(f"LLM generate failed (attempt {attempt + 1}): {e}") + if attempt == 2: + logger.warning("LLM generate failed after 3 retries, returning empty dict") + response_json = [] + break + + task_chunks = {} + for item in response_json: + task_name = item["task_name"] + message_indices = item["message_indices"] + for start, end in message_indices: + task_chunks.setdefault(task_name, []).extend(messages[start : end + 1]) + return task_chunks + + +def _extract_skill_memory_by_llm( + messages: MessageList, old_memories: list[TextualMemoryItem], llm: BaseLLM +) -> dict[str, Any]: + old_memories_dict = [skill_memory.model_dump() for skill_memory in old_memories] + old_mem_references = [ + { + "id": mem["id"], + "name": mem["metadata"]["name"], + "description": mem["metadata"]["description"], + "procedure": mem["metadata"]["procedure"], + "experience": mem["metadata"]["experience"], + "preference": mem["metadata"]["preference"], + "example": mem["metadata"]["example"], + "tags": mem["metadata"]["tags"], + "scripts": mem["metadata"].get("scripts"), + "others": mem["metadata"]["others"], + } + for mem in old_memories_dict + ] + + # Prepare conversation context + messages_context = "\n".join( + [f"{message['role']}: {message['content']}" for message in messages] + ) + + # Prepare old memories context + old_memories_context = json.dumps(old_mem_references, ensure_ascii=False, indent=2) + + # Prepare prompt + lang = detect_lang(messages_context) + template = SKILL_MEMORY_EXTRACTION_PROMPT_ZH if lang == "zh" else SKILL_MEMORY_EXTRACTION_PROMPT + prompt_content = template.replace("{old_memories}", old_memories_context).replace( + "{messages}", messages_context + ) + + prompt = [{"role": "user", "content": prompt_content}] + + # Call LLM to extract skill memory with retry logic + for attempt in range(3): + try: + response_text = llm.generate(prompt) + # Clean up response (remove markdown code blocks if present) + response_text = response_text.strip() + response_text = response_text.replace("```json", "").replace("```", "").strip() + + # Parse JSON response + skill_memory = json.loads(response_text) + + # If LLM returns null (parsed as None), log and return None + if skill_memory is None: + logger.info("No skill memory extracted from conversation (LLM returned null)") + return None + + return skill_memory + + except json.JSONDecodeError as e: + logger.warning(f"JSON decode failed (attempt {attempt + 1}): {e}") + logger.debug(f"Response text: {response_text}") + if attempt == 2: + logger.warning("Failed to parse skill memory after 3 retries") + return None + except Exception as e: + logger.warning(f"LLM skill memory extraction failed (attempt {attempt + 1}): {e}") + if attempt == 2: + logger.warning("LLM skill memory extraction failed after 3 retries") + return None + + return None + + +def _recall_related_skill_memories( + task_type: str, + messages: MessageList, + searcher: Searcher, + llm: BaseLLM, + rewrite_query: bool, + info: dict[str, Any], + mem_cube_id: str, +) -> list[TextualMemoryItem]: + query = _rewrite_query(task_type, messages, llm, rewrite_query) + related_skill_memories = searcher.search( + query, + top_k=10, + memory_type="SkillMemory", + info=info, + include_skill_memory=True, + user_name=mem_cube_id, + ) + + return related_skill_memories + + +def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_query: bool) -> str: + if not rewrite_query: + # Return the first user message content if rewrite is disabled + return messages[0]["content"] if messages else "" + + # Construct messages context for LLM + messages_context = "\n".join( + [f"{message['role']}: {message['content']}" for message in messages] + ) + + # Prepare prompt with task type and messages + lang = detect_lang(messages_context) + template = TASK_QUERY_REWRITE_PROMPT_ZH if lang == "zh" else TASK_QUERY_REWRITE_PROMPT + prompt_content = template.replace("{task_type}", task_type).replace( + "{messages}", messages_context + ) + prompt = [{"role": "user", "content": prompt_content}] + + # Call LLM to rewrite the query with retry logic + for attempt in range(3): + try: + response_text = llm.generate(prompt) + # Clean up response (remove any markdown formatting if present) + response_text = response_text.strip() + logger.info(f"Rewritten query for task '{task_type}': {response_text}") + return response_text + except Exception as e: + logger.warning(f"LLM query rewrite failed (attempt {attempt + 1}): {e}") + if attempt == 2: + logger.warning( + "LLM query rewrite failed after 3 retries, returning first message content" + ) + return messages[0]["content"] if messages else "" + + # Fallback (should not reach here due to return in exception handling) + return messages[0]["content"] if messages else "" + + +@require_python_package( + import_name="alibabacloud_oss_v2", + install_command="pip install alibabacloud-oss-v2", +) +def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: Any) -> str: + import alibabacloud_oss_v2 as oss + + result = client.put_object_from_file( + request=oss.PutObjectRequest( + bucket=os.getenv("OSS_BUCKET_NAME"), + key=oss_file_path, + ), + filepath=local_file_path, + ) + + if result.status_code != 200: + logger.warning("Failed to upload skill to OSS") + return "" + + # Construct and return the URL + bucket_name = os.getenv("OSS_BUCKET_NAME") + endpoint = os.getenv("OSS_ENDPOINT").replace("https://", "").replace("http://", "") + url = f"https://{bucket_name}.{endpoint}/{oss_file_path}" + return url + + +@require_python_package( + import_name="alibabacloud_oss_v2", + install_command="pip install alibabacloud-oss-v2", +) +def _delete_skills_from_oss(oss_file_path: str, client: Any) -> Any: + import alibabacloud_oss_v2 as oss + + result = client.delete_object( + oss.DeleteObjectRequest( + bucket=os.getenv("OSS_BUCKET_NAME"), + key=oss_file_path, + ) + ) + return result + + +def _write_skills_to_file( + skill_memory: dict[str, Any], info: dict[str, Any], skills_dir_config: dict[str, Any] +) -> str: + user_id = info.get("user_id", "unknown") + skill_name = skill_memory.get("name", "unnamed_skill").replace(" ", "_").lower() + + # Create tmp directory for user if it doesn't exist + tmp_dir = Path(skills_dir_config["skills_local_dir"]) / user_id + tmp_dir.mkdir(parents=True, exist_ok=True) + + # Create skill directory directly in tmp_dir + skill_dir = tmp_dir / skill_name + skill_dir.mkdir(parents=True, exist_ok=True) + + # Generate SKILL.md content with frontmatter + skill_md_content = f"""--- +name: {skill_name} +description: {skill_memory.get("description", "")} +--- +""" + + # Add Procedure section only if present + procedure = skill_memory.get("procedure", "") + if procedure and procedure.strip(): + skill_md_content += f"\n## Procedure\n{procedure}\n" + + # Add Experience section only if there are items + experiences = skill_memory.get("experience", []) + if experiences: + skill_md_content += "\n## Experience\n" + for idx, exp in enumerate(experiences, 1): + skill_md_content += f"{idx}. {exp}\n" + + # Add User Preferences section only if there are items + preferences = skill_memory.get("preference", []) + if preferences: + skill_md_content += "\n## User Preferences\n" + for pref in preferences: + skill_md_content += f"- {pref}\n" + + # Add Examples section only if there are items + examples = skill_memory.get("example", []) + if examples: + skill_md_content += "\n## Examples\n" + for idx, example in enumerate(examples, 1): + skill_md_content += f"\n### Example {idx}\n{example}\n" + + # Add scripts reference if present + scripts = skill_memory.get("scripts") + if scripts and isinstance(scripts, dict): + skill_md_content += "\n## Scripts\n" + skill_md_content += "This skill includes the following executable scripts:\n\n" + for script_name in scripts: + skill_md_content += f"- `./scripts/{script_name}`\n" + + # Add others - handle both inline content and separate markdown files + others = skill_memory.get("others") + if others and isinstance(others, dict): + # Separate markdown files from inline content + md_files = {} + inline_content = {} + + for key, value in others.items(): + if key.endswith(".md"): + md_files[key] = value + else: + inline_content[key] = value + + # Add inline content to SKILL.md + if inline_content: + skill_md_content += "\n## Additional Information\n" + for key, value in inline_content.items(): + skill_md_content += f"\n### {key}\n{value}\n" + + # Add references to separate markdown files + if md_files: + if not inline_content: + skill_md_content += "\n## Additional Information\n" + skill_md_content += "\nSee also:\n" + for md_filename in md_files: + skill_md_content += f"- [{md_filename}](./{md_filename})\n" + + # Write SKILL.md file + skill_md_path = skill_dir / "SKILL.md" + with open(skill_md_path, "w", encoding="utf-8") as f: + f.write(skill_md_content) + + # Write separate markdown files from others + if others and isinstance(others, dict): + for key, value in others.items(): + if key.endswith(".md"): + md_file_path = skill_dir / key + with open(md_file_path, "w", encoding="utf-8") as f: + f.write(value) + + # If there are scripts, create a scripts directory with individual script files + if scripts and isinstance(scripts, dict): + scripts_dir = skill_dir / "scripts" + scripts_dir.mkdir(parents=True, exist_ok=True) + + # Write each script to its own file + for script_filename, script_content in scripts.items(): + # Ensure filename ends with .py + if not script_filename.endswith(".py"): + script_filename = f"{script_filename}.py" + + script_path = scripts_dir / script_filename + with open(script_path, "w", encoding="utf-8") as f: + f.write(script_content) + + # Create zip file in tmp_dir + zip_filename = f"{skill_name}.zip" + zip_path = tmp_dir / zip_filename + + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: + # Walk through the skill directory and add all files + for file_path in skill_dir.rglob("*"): + if file_path.is_file(): + # Use relative path from skill_dir for archive + arcname = Path(skill_dir.name) / file_path.relative_to(skill_dir) + zipf.write(str(file_path), str(arcname)) + + logger.info(f"Created skill zip file: {zip_path}") + return str(zip_path) + + +def create_skill_memory_item( + skill_memory: dict[str, Any], info: dict[str, Any], embedder: BaseEmbedder | None = None +) -> TextualMemoryItem: + info_ = info.copy() + user_id = info_.pop("user_id", "") + session_id = info_.pop("session_id", "") + + # Use description as the memory content + memory_content = skill_memory.get("description", "") + + # Create metadata with all skill-specific fields directly + metadata = TreeNodeTextualMemoryMetadata( + user_id=user_id, + session_id=session_id, + memory_type="SkillMemory", + status="activated", + tags=skill_memory.get("tags", []), + key=skill_memory.get("name", ""), + sources=[], + usage=[], + background="", + confidence=0.99, + created_at=datetime.now().isoformat(), + updated_at=datetime.now().isoformat(), + type="skills", + info=info_, + embedding=embedder.embed([memory_content])[0] if embedder else None, + # Skill-specific fields + name=skill_memory.get("name", ""), + description=skill_memory.get("description", ""), + procedure=skill_memory.get("procedure", ""), + experience=skill_memory.get("experience", []), + preference=skill_memory.get("preference", []), + example=skill_memory.get("example", []), + scripts=skill_memory.get("scripts"), + others=skill_memory.get("others"), + url=skill_memory.get("url", ""), + ) + + # If this is an update, use the old memory ID + item_id = ( + skill_memory.get("old_memory_id", "") + if skill_memory.get("update", False) + else str(uuid.uuid4()) + ) + if not item_id: + item_id = str(uuid.uuid4()) + + return TextualMemoryItem(id=item_id, memory=memory_content, metadata=metadata) + + +def process_skill_memory_fine( + fast_memory_items: list[TextualMemoryItem], + info: dict[str, Any], + searcher: Searcher | None = None, + graph_db: BaseGraphDB | None = None, + llm: BaseLLM | None = None, + embedder: BaseEmbedder | None = None, + rewrite_query: bool = True, + oss_config: dict[str, Any] | None = None, + skills_dir_config: dict[str, Any] | None = None, + **kwargs, +) -> list[TextualMemoryItem]: + # Validate required configurations + if not oss_config: + logger.warning("OSS configuration is required for skill memory processing") + return [] + + if not skills_dir_config: + logger.warning("Skills directory configuration is required for skill memory processing") + return [] + + # Validate skills_dir has required keys + required_keys = ["skills_local_dir", "skills_oss_dir"] + missing_keys = [key for key in required_keys if key not in skills_dir_config] + if missing_keys: + logger.warning( + f"Skills directory configuration missing required keys: {', '.join(missing_keys)}" + ) + return [] + + oss_client = create_oss_client(oss_config) + if not oss_client: + logger.warning("Failed to create OSS client") + return [] + + messages = _reconstruct_messages_from_memory_items(fast_memory_items) + messages = _add_index_to_message(messages) + + task_chunks = _split_task_chunk_by_llm(llm, messages) + + # recall - get related skill memories for each task separately (parallel) + related_skill_memories_by_task = {} + with ContextThreadPoolExecutor(max_workers=min(len(task_chunks), 5)) as executor: + recall_futures = { + executor.submit( + _recall_related_skill_memories, + task_type=task, + messages=msg, + searcher=searcher, + llm=llm, + rewrite_query=rewrite_query, + info=info, + mem_cube_id=kwargs.get("user_name", info.get("user_id", "")), + ): task + for task, msg in task_chunks.items() + } + for future in as_completed(recall_futures): + task_name = recall_futures[future] + try: + related_memories = future.result() + related_skill_memories_by_task[task_name] = related_memories + except Exception as e: + logger.warning(f"Error recalling skill memories for task '{task_name}': {e}") + related_skill_memories_by_task[task_name] = [] + + skill_memories = [] + with ContextThreadPoolExecutor(max_workers=min(len(task_chunks), 5)) as executor: + futures = { + executor.submit( + _extract_skill_memory_by_llm, + messages, + related_skill_memories_by_task.get(task_type, []), + llm, + ): task_type + for task_type, messages in task_chunks.items() + } + for future in as_completed(futures): + try: + skill_memory = future.result() + if skill_memory: # Only add non-None results + skill_memories.append(skill_memory) + except Exception as e: + logger.warning(f"Error extracting skill memory: {e}") + continue + + # write skills to file and get zip paths + skill_memory_with_paths = [] + with ContextThreadPoolExecutor(max_workers=min(len(skill_memories), 5)) as executor: + futures = { + executor.submit( + _write_skills_to_file, skill_memory, info, skills_dir_config + ): skill_memory + for skill_memory in skill_memories + } + for future in as_completed(futures): + try: + zip_path = future.result() + skill_memory = futures[future] + skill_memory_with_paths.append((skill_memory, zip_path)) + except Exception as e: + logger.warning(f"Error writing skills to file: {e}") + continue + + # Create a mapping from old_memory_id to old memory for easy lookup + # Collect all related memories from all tasks + all_related_memories = [] + for memories in related_skill_memories_by_task.values(): + all_related_memories.extend(memories) + old_memories_map = {mem.id: mem for mem in all_related_memories} + + # upload skills to oss and set urls directly to skill_memory + user_id = info.get("user_id", "unknown") + + for skill_memory, zip_path in skill_memory_with_paths: + try: + # Delete old skill from OSS if this is an update + if skill_memory.get("update", False) and skill_memory.get("old_memory_id"): + old_memory_id = skill_memory["old_memory_id"] + old_memory = old_memories_map.get(old_memory_id) + + if old_memory: + # Get old OSS path from the old memory's metadata + old_oss_path = getattr(old_memory.metadata, "url", None) + + if old_oss_path: + try: + # delete old skill from OSS + zip_filename = Path(old_oss_path).name + old_oss_path = ( + Path(skills_dir_config["skills_oss_dir"]) / user_id / zip_filename + ).as_posix() + _delete_skills_from_oss(old_oss_path, oss_client) + logger.info(f"Deleted old skill from OSS: {old_oss_path}") + except Exception as e: + logger.warning(f"Failed to delete old skill from OSS: {e}") + + # delete old skill from graph db + if graph_db: + graph_db.delete_node_by_prams(memory_ids=[old_memory_id]) + logger.info(f"Deleted old skill from graph db: {old_memory_id}") + + # Upload new skill to OSS + # Use the same filename as the local zip file + zip_filename = Path(zip_path).name + oss_path = ( + Path(skills_dir_config["skills_oss_dir"]) / user_id / zip_filename + ).as_posix() + + # _upload_skills_to_oss returns the URL + url = _upload_skills_to_oss( + local_file_path=str(zip_path), oss_file_path=oss_path, client=oss_client + ) + + # Set URL directly to skill_memory + skill_memory["url"] = url + + logger.info(f"Uploaded skill to OSS: {url}") + except Exception as e: + logger.warning(f"Error uploading skill to OSS: {e}") + skill_memory["url"] = "" # Set to empty string if upload fails + finally: + # Clean up local files after upload + try: + zip_file = Path(zip_path) + skill_dir = zip_file.parent / zip_file.stem + # Delete zip file + if zip_file.exists(): + zip_file.unlink() + # Delete skill directory + if skill_dir.exists(): + shutil.rmtree(skill_dir) + logger.info(f"Cleaned up local files: {zip_path} and {skill_dir}") + except Exception as cleanup_error: + logger.warning(f"Error cleaning up local files: {cleanup_error}") + + # Create TextualMemoryItem objects + skill_memory_items = [] + for skill_memory in skill_memories: + try: + memory_item = create_skill_memory_item(skill_memory, info, embedder) + skill_memory_items.append(memory_item) + except Exception as e: + logger.warning(f"Error creating skill memory item: {e}") + continue + + return skill_memory_items diff --git a/src/memos/mem_reader/simple_struct.py b/src/memos/mem_reader/simple_struct.py index 783da763e..e80f9e9da 100644 --- a/src/memos/mem_reader/simple_struct.py +++ b/src/memos/mem_reader/simple_struct.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from memos.graph_dbs.base import BaseGraphDB + from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher from memos.mem_reader.read_multi_modal import coerce_scene_data, detect_lang from memos.mem_reader.utils import ( count_tokens_text, @@ -187,6 +188,9 @@ def __init__(self, config: SimpleStructMemReaderConfig): def set_graph_db(self, graph_db: "BaseGraphDB | None") -> None: self.graph_db = graph_db + def set_searcher(self, searcher: "Searcher | None") -> None: + self.searcher = searcher + def _make_memory_item( self, value: str, diff --git a/src/memos/memories/textual/item.py b/src/memos/memories/textual/item.py index a1c85033b..46770758d 100644 --- a/src/memos/memories/textual/item.py +++ b/src/memos/memories/textual/item.py @@ -112,6 +112,7 @@ class TreeNodeTextualMemoryMetadata(TextualMemoryMetadata): "OuterMemory", "ToolSchemaMemory", "ToolTrajectoryMemory", + "SkillMemory", ] = Field(default="WorkingMemory", description="Memory lifecycle type.") sources: list[SourceMessage] | None = Field( default=None, description="Multiple origins of the memory (e.g., URLs, notes)." diff --git a/src/memos/memories/textual/tree.py b/src/memos/memories/textual/tree.py index b963cfa9b..5b999cd6d 100644 --- a/src/memos/memories/textual/tree.py +++ b/src/memos/memories/textual/tree.py @@ -161,6 +161,8 @@ def search( user_name: str | None = None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, dedup: str | None = None, **kwargs, ) -> list[TextualMemoryItem]: @@ -208,6 +210,8 @@ def search( user_name=user_name, search_tool_memory=search_tool_memory, tool_mem_top_k=tool_mem_top_k, + include_skill_memory=include_skill_memory, + skill_mem_top_k=skill_mem_top_k, dedup=dedup, **kwargs, ) diff --git a/src/memos/memories/textual/tree_text_memory/organize/manager.py b/src/memos/memories/textual/tree_text_memory/organize/manager.py index c96d5a12a..59675bdc2 100644 --- a/src/memos/memories/textual/tree_text_memory/organize/manager.py +++ b/src/memos/memories/textual/tree_text_memory/organize/manager.py @@ -159,7 +159,12 @@ def _add_memories_batch( for memory in memories: working_id = str(uuid.uuid4()) - if memory.metadata.memory_type not in ("ToolSchemaMemory", "ToolTrajectoryMemory"): + if memory.metadata.memory_type in ( + "WorkingMemory", + "LongTermMemory", + "UserMemory", + "OuterMemory", + ): working_metadata = memory.metadata.model_copy( update={"memory_type": "WorkingMemory"} ).model_dump(exclude_none=True) @@ -176,6 +181,7 @@ def _add_memories_batch( "UserMemory", "ToolSchemaMemory", "ToolTrajectoryMemory", + "SkillMemory", ): graph_node_id = str(uuid.uuid4()) metadata_dict = memory.metadata.model_dump(exclude_none=True) @@ -310,7 +316,12 @@ def _process_memory(self, memory: TextualMemoryItem, user_name: str | None = Non working_id = str(uuid.uuid4()) with ContextThreadPoolExecutor(max_workers=2, thread_name_prefix="mem") as ex: - if memory.metadata.memory_type not in ("ToolSchemaMemory", "ToolTrajectoryMemory"): + if memory.metadata.memory_type in ( + "WorkingMemory", + "LongTermMemory", + "UserMemory", + "OuterMemory", + ): f_working = ex.submit( self._add_memory_to_db, memory, "WorkingMemory", user_name, working_id ) @@ -321,6 +332,7 @@ def _process_memory(self, memory: TextualMemoryItem, user_name: str | None = Non "UserMemory", "ToolSchemaMemory", "ToolTrajectoryMemory", + "SkillMemory", ): f_graph = ex.submit( self._add_to_graph_memory, diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/recall.py b/src/memos/memories/textual/tree_text_memory/retrieve/recall.py index 4541b118b..c9f2ec156 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/recall.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/recall.py @@ -67,6 +67,7 @@ def retrieve( "UserMemory", "ToolSchemaMemory", "ToolTrajectoryMemory", + "SkillMemory", ]: raise ValueError(f"Unsupported memory scope: {memory_scope}") diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py b/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py index 8c30d74f3..dcd4e1fba 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py @@ -81,6 +81,8 @@ def retrieve( user_name: str | None = None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, **kwargs, ) -> list[tuple[TextualMemoryItem, float]]: logger.info( @@ -108,6 +110,8 @@ def retrieve( user_name, search_tool_memory, tool_mem_top_k, + include_skill_memory, + skill_mem_top_k, ) return results @@ -119,6 +123,8 @@ def post_retrieve( info=None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, dedup: str | None = None, plugin=False, ): @@ -127,7 +133,13 @@ def post_retrieve( else: deduped = self._deduplicate_results(retrieved_results) final_results = self._sort_and_trim( - deduped, top_k, plugin, search_tool_memory, tool_mem_top_k + deduped, + top_k, + plugin, + search_tool_memory, + tool_mem_top_k, + include_skill_memory, + skill_mem_top_k, ) self._update_usage_history(final_results, info, user_name) return final_results @@ -145,6 +157,8 @@ def search( user_name: str | None = None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, dedup: str | None = None, **kwargs, ) -> list[TextualMemoryItem]: @@ -192,6 +206,8 @@ def search( user_name=user_name, search_tool_memory=search_tool_memory, tool_mem_top_k=tool_mem_top_k, + include_skill_memory=include_skill_memory, + skill_mem_top_k=skill_mem_top_k, **kwargs, ) @@ -207,6 +223,8 @@ def search( plugin=kwargs.get("plugin", False), search_tool_memory=search_tool_memory, tool_mem_top_k=tool_mem_top_k, + include_skill_memory=include_skill_memory, + skill_mem_top_k=skill_mem_top_k, dedup=dedup, ) @@ -305,8 +323,10 @@ def _retrieve_paths( user_name: str | None = None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, ): - """Run A/B/C retrieval paths in parallel""" + """Run A/B/C/D/E retrieval paths in parallel""" tasks = [] id_filter = { "user_id": info.get("user_id", None), @@ -314,7 +334,7 @@ def _retrieve_paths( } id_filter = {k: v for k, v in id_filter.items() if v is not None} - with ContextThreadPoolExecutor(max_workers=3) as executor: + with ContextThreadPoolExecutor(max_workers=5) as executor: tasks.append( executor.submit( self._retrieve_from_working_memory, @@ -373,6 +393,22 @@ def _retrieve_paths( mode=mode, ) ) + if include_skill_memory: + tasks.append( + executor.submit( + self._retrieve_from_skill_memory, + query, + parsed_goal, + query_embedding, + skill_mem_top_k, + memory_type, + search_filter, + search_priority, + user_name, + id_filter, + mode=mode, + ) + ) results = [] for t in tasks: results.extend(t.result()) @@ -642,6 +678,58 @@ def _retrieve_from_tool_memory( ) return schema_reranked + trajectory_reranked + # --- Path E + @timed + def _retrieve_from_skill_memory( + self, + query, + parsed_goal, + query_embedding, + top_k, + memory_type, + search_filter: dict | None = None, + search_priority: dict | None = None, + user_name: str | None = None, + id_filter: dict | None = None, + mode: str = "fast", + ): + """Retrieve and rerank from SkillMemory""" + if memory_type not in ["All", "SkillMemory"]: + logger.info(f"[PATH-E] '{query}' Skipped (memory_type does not match)") + return [] + + # chain of thinking + cot_embeddings = [] + if self.vec_cot: + queries = self._cot_query(query, mode=mode, context=parsed_goal.context) + if len(queries) > 1: + cot_embeddings = self.embedder.embed(queries) + cot_embeddings.extend(query_embedding) + else: + cot_embeddings = query_embedding + + items = self.graph_retriever.retrieve( + query=query, + parsed_goal=parsed_goal, + query_embedding=cot_embeddings, + top_k=top_k * 2, + memory_scope="SkillMemory", + search_filter=search_filter, + search_priority=search_priority, + user_name=user_name, + id_filter=id_filter, + use_fast_graph=self.use_fast_graph, + ) + + return self.reranker.rerank( + query=query, + query_embedding=query_embedding[0], + graph_results=items, + top_k=top_k, + parsed_goal=parsed_goal, + search_filter=search_filter, + ) + @timed def _retrieve_simple( self, @@ -704,7 +792,14 @@ def _deduplicate_results(self, results): @timed def _sort_and_trim( - self, results, top_k, plugin=False, search_tool_memory=False, tool_mem_top_k=6 + self, + results, + top_k, + plugin=False, + search_tool_memory=False, + tool_mem_top_k=6, + include_skill_memory=False, + skill_mem_top_k=3, ): """Sort results by score and trim to top_k""" final_items = [] @@ -749,11 +844,35 @@ def _sort_and_trim( metadata=SearchedTreeNodeTextualMemoryMetadata(**meta_data), ) ) + + if include_skill_memory: + skill_results = [ + (item, score) + for item, score in results + if item.metadata.memory_type == "SkillMemory" + ] + sorted_skill_results = sorted(skill_results, key=lambda pair: pair[1], reverse=True)[ + :skill_mem_top_k + ] + for item, score in sorted_skill_results: + if plugin and round(score, 2) == 0.00: + continue + meta_data = item.metadata.model_dump() + meta_data["relativity"] = score + final_items.append( + TextualMemoryItem( + id=item.id, + memory=item.memory, + metadata=SearchedTreeNodeTextualMemoryMetadata(**meta_data), + ) + ) + # separate textual results results = [ (item, score) for item, score in results - if item.metadata.memory_type not in ["ToolSchemaMemory", "ToolTrajectoryMemory"] + if item.metadata.memory_type + in ["WorkingMemory", "LongTermMemory", "UserMemory", "OuterMemory"] ] sorted_results = sorted(results, key=lambda pair: pair[1], reverse=True)[:top_k] diff --git a/src/memos/multi_mem_cube/composite_cube.py b/src/memos/multi_mem_cube/composite_cube.py index c1017bfae..0d2d460e9 100644 --- a/src/memos/multi_mem_cube/composite_cube.py +++ b/src/memos/multi_mem_cube/composite_cube.py @@ -46,6 +46,7 @@ def search_memories(self, search_req: APISearchRequest) -> dict[str, Any]: "pref_mem": [], "pref_note": "", "tool_mem": [], + "skill_mem": [], } def _search_single_cube(view: SingleCubeView) -> dict[str, Any]: @@ -65,7 +66,7 @@ def _search_single_cube(view: SingleCubeView) -> dict[str, Any]: merged_results["para_mem"].extend(cube_result.get("para_mem", [])) merged_results["pref_mem"].extend(cube_result.get("pref_mem", [])) merged_results["tool_mem"].extend(cube_result.get("tool_mem", [])) - + merged_results["skill_mem"].extend(cube_result.get("skill_mem", [])) note = cube_result.get("pref_note") if note: if merged_results["pref_note"]: diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index 426cf32be..c75fc23c6 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -121,6 +121,7 @@ def search_memories(self, search_req: APISearchRequest) -> dict[str, Any]: "pref_mem": [], "pref_note": "", "tool_mem": [], + "skill_mem": [], } # Determine search mode @@ -475,6 +476,8 @@ def _fast_search( plugin=plugin, search_tool_memory=search_req.search_tool_memory, tool_mem_top_k=search_req.tool_mem_top_k, + include_skill_memory=search_req.include_skill_memory, + skill_mem_top_k=search_req.skill_mem_top_k, dedup=search_req.dedup, ) diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py new file mode 100644 index 000000000..abfc11ef2 --- /dev/null +++ b/src/memos/templates/skill_mem_prompt.py @@ -0,0 +1,255 @@ +TASK_CHUNKING_PROMPT = """ +# Role +You are an expert in natural language processing (NLP) and dialogue logic analysis. You excel at organizing logical threads from complex long conversations and accurately extracting users' core intentions. + +# Task +Please analyze the provided conversation records, identify all independent "tasks" that the user has asked the AI to perform, and assign the corresponding dialogue message numbers to each task. + +**Note**: Tasks should be high-level and general, typically divided by theme or topic. For example: "Travel Planning", "PDF Operations", "Code Review", "Data Analysis", etc. Avoid being too specific or granular. + +# Rules & Constraints +1. **Task Independence**: If multiple unrelated topics are discussed in the conversation, identify them as different tasks. +2. **Non-continuous Processing**: Pay attention to identifying "jumping" conversations. For example, if the user made travel plans in messages 8-11, switched to consulting about weather in messages 12-22, and then returned to making travel plans in messages 23-24, be sure to assign both 8-11 and 23-24 to the task "Making travel plans". However, if messages are continuous and belong to the same task, do not split them apart. +3. **Filter Chit-chat**: Only extract tasks with clear goals, instructions, or knowledge-based discussions. Ignore meaningless greetings (such as "Hello", "Are you there?") or closing remarks unless they are part of the task context. +4. **Output Format**: Please strictly follow the JSON format for output to facilitate my subsequent processing. +5. **Language Consistency**: The language used in the task_name field must match the language used in the conversation records. + +```json +[ + { + "task_id": 1, + "task_name": "Brief description of the task (e.g., Making travel plans)", + "message_indices": [[0, 5],[16, 17]], # 0-5 and 16-17 are the message indices for this task + "reasoning": "Briefly explain why these messages are grouped together" + }, + ... +] +``` + +# Context (Conversation Records) +{{messages}} +""" + + +TASK_CHUNKING_PROMPT_ZH = """ +# 角色 +你是自然语言处理(NLP)和对话逻辑分析的专家。你擅长从复杂的长对话中整理逻辑线索,准确提取用户的核心意图。 + +# 任务 +请分析提供的对话记录,识别所有用户要求 AI 执行的独立"任务",并为每个任务分配相应的对话消息编号。 + +**注意**:任务应该是高层次和通用的,通常按主题或话题划分。例如:"旅行计划"、"PDF操作"、"代码审查"、"数据分析"等。避免过于具体或细化。 + +# 规则与约束 +1. **任务独立性**:如果对话中讨论了多个不相关的话题,请将它们识别为不同的任务。 +2. **非连续处理**:注意识别"跳跃式"对话。例如,如果用户在消息 8-11 中制定旅行计划,在消息 12-22 中切换到咨询天气,然后在消息 23-24 中返回到制定旅行计划,请务必将 8-11 和 23-24 都分配给"制定旅行计划"任务。但是,如果消息是连续的且属于同一任务,不能将其分开。 +3. **过滤闲聊**:仅提取具有明确目标、指令或基于知识的讨论的任务。忽略无意义的问候(例如"你好"、"在吗?")或结束语,除非它们是任务上下文的一部分。 +4. **输出格式**:请严格遵循 JSON 格式输出,以便我后续处理。 +5. **语言一致性**:task_name 字段使用的语言必须与对话记录中使用的语言相匹配。 + +```json +[ + { + "task_id": 1, + "task_name": "任务的简要描述(例如:制定旅行计划)", + "message_indices": [[0, 5],[16, 17]], # 0-5 和 16-17 是此任务的消息索引 + "reasoning": "简要解释为什么这些消息被分组在一起" + }, + ... +] +``` + +# 上下文(对话记录) +{{messages}} +""" + + +SKILL_MEMORY_EXTRACTION_PROMPT = """ +# Role +You are an expert in general skill extraction and skill memory management. You excel at analyzing conversations to extract actionable, transferable, and reusable skills, procedures, experiences, and user preferences. The skills you extract should be general and applicable across similar scenarios, not overly specific to a single instance. + +# Task +Based on the provided conversation messages and existing skill memories, extract new skill memory or update existing ones. You need to determine whether the current conversation contains skills similar to existing memories. + +# Existing Skill Memories +{old_memories} + +# Conversation Messages +{messages} + +# Extraction Rules +1. **Similarity Check**: Compare the current conversation with existing skill memories. If a similar skill exists, set "update": true and provide the "old_memory_id". Otherwise, set "update": false and leave "old_memory_id" empty. +2. **Completeness**: Extract comprehensive information including procedures, experiences, preferences, and examples. +3. **Clarity**: Ensure procedures are step-by-step and easy to follow. +4. **Specificity**: Capture specific user preferences and lessons learned from experiences. +5. **Language Consistency**: Use the same language as the conversation. +6. **Accuracy**: Only extract information that is explicitly present or strongly implied in the conversation. + +# Output Format +Please output in strict JSON format: + +```json +{ + "name": "A concise name for this skill or task type", + "description": "A clear description of what this skill does or accomplishes", + "procedure": "Step-by-step procedure: 1. First step 2. Second step 3. Third step...", + "experience": ["Lesson 1: Specific experience or insight learned", "Lesson 2: Another valuable experience..."], + "preference": ["User preference 1", "User preference 2", "User preference 3..."], + "example": ["Example case 1 demonstrating how to complete the task following this skill's guidance", "Example case 2..."], + "tags": ["tag1", "tag2", "tag3"], + "scripts": {"script_name.py": "# Python code here\nprint('Hello')", "another_script.py": "# More code\nimport os"}, + "others": {"Section Title": "Content here", "reference.md": "# Reference content for this skill"}, + "update": false, + "old_memory_id": "" +} +``` + +# Field Descriptions +- **name**: Brief identifier for the skill (e.g., "Travel Planning", "Code Review Process") +- **description**: What this skill accomplishes or its purpose +- **procedure**: Sequential steps to complete the task +- **experience**: Lessons learned, best practices, things to avoid +- **preference**: User's specific preferences, likes, dislikes +- **example**: Concrete example cases demonstrating how to complete the task by following this skill's guidance +- **tags**: Relevant keywords for categorization +- **scripts**: Dictionary of scripts where key is the .py filename and value is the executable code snippet. Use null if not applicable +- **others**: Flexible additional information in key-value format. Can be either: + - Simple key-value pairs where key is a title and value is content + - Separate markdown files where key is .md filename and value is the markdown content + Use null if not applicable +- **update**: true if updating existing memory, false if creating new +- **old_memory_id**: The ID of the existing memory being updated, or empty string if new + +# Important Notes +- If no clear skill can be extracted from the conversation, return null +- Ensure all string values are properly formatted and contain meaningful information +- Arrays should contain at least one item if the field is populated +- Be thorough but avoid redundancy + +# Output +Please output only the JSON object, without any additional formatting, markdown code blocks, or explanation. +""" + + +SKILL_MEMORY_EXTRACTION_PROMPT_ZH = """ +# 角色 +你是通用技能提取和技能记忆管理的专家。你擅长分析对话,提取可操作的、可迁移的、可复用的技能、流程、经验和用户偏好。你提取的技能应该是通用的,能够应用于类似场景,而不是过于针对单一实例。 + +# 任务 +基于提供的对话消息和现有的技能记忆,提取新的技能记忆或更新现有的技能记忆。你需要判断当前对话中是否包含与现有记忆相似的技能。 + +# 现有技能记忆 +{old_memories} + +# 对话消息 +{messages} + +# 提取规则 +1. **相似性检查**:将当前对话与现有技能记忆进行比较。如果存在相似的技能,设置 "update": true 并提供 "old_memory_id"。否则,设置 "update": false 并将 "old_memory_id" 留空。 +2. **完整性**:提取全面的信息,包括流程、经验、偏好和示例。 +3. **清晰性**:确保流程是逐步的,易于遵循。 +4. **具体性**:捕获具体的用户偏好和从经验中学到的教训。 +5. **语言一致性**:使用与对话相同的语言。 +6. **准确性**:仅提取对话中明确存在或强烈暗示的信息。 + +# 输出格式 +请以严格的 JSON 格式输出: + +```json +{ + "name": "技能或任务类型的简洁名称", + "description": "对该技能的作用或目的的清晰描述", + "procedure": "逐步流程:1. 第一步 2. 第二步 3. 第三步...", + "experience": ["经验教训 1:学到的具体经验或见解", "经验教训 2:另一个有价值的经验..."], + "preference": ["用户偏好 1", "用户偏好 2", "用户偏好 3..."], + "example": ["示例案例 1:展示按照此技能的指引完成任务的过程", "示例案例 2..."], + "tags": ["标签1", "标签2", "标签3"], + "scripts": {"script_name.py": "# Python 代码\nprint('Hello')", "another_script.py": "# 更多代码\nimport os"}, + "others": {"章节标题": "这里的内容", "reference.md": "# 此技能的参考内容"}, + "update": false, + "old_memory_id": "" +} +``` + +# 字段说明 +- **name**:技能的简短标识符(例如:"旅行计划"、"代码审查流程") +- **description**:该技能完成什么或其目的 +- **procedure**:完成任务的顺序步骤 +- **experience**:学到的经验教训、最佳实践、要避免的事项 +- **preference**:用户的具体偏好、喜好、厌恶 +- **example**:具体的示例案例,展示如何按照此技能的指引完成任务 +- **tags**:用于分类的相关关键词 +- **scripts**:脚本字典,其中 key 是 .py 文件名,value 是可执行代码片段。如果不适用则使用 null +- **others**:灵活的附加信息,采用键值对格式。可以是: + - 简单的键值对,其中 key 是标题,value 是内容 + - 独立的 markdown 文件,其中 key 是 .md 文件名,value 是 markdown 内容 + 如果不适用则使用 null +- **update**:如果更新现有记忆则为 true,如果创建新记忆则为 false +- **old_memory_id**:正在更新的现有记忆的 ID,如果是新记忆则为空字符串 + +# 重要说明 +- 如果无法从对话中提取清晰的技能,返回 null +- 确保所有字符串值格式正确且包含有意义的信息 +- 如果填充数组,则数组应至少包含一项 +- 要全面但避免冗余 + +# 输出 +请仅输出 JSON 对象,不要添加任何额外的格式、markdown 代码块或解释。 +""" + + +TASK_QUERY_REWRITE_PROMPT = """ +# Role +You are an expert in understanding user intentions and task requirements. You excel at analyzing conversations and extracting the core task description. + +# Task +Based on the provided task type and conversation messages, analyze and determine what specific task the user wants to complete, then rewrite it into a clear, concise task query string. + +# Task Type +{task_type} + +# Conversation Messages +{messages} + +# Requirements +1. Analyze the conversation content to understand the user's core intention +2. Consider the task type as context +3. Extract and summarize the key task objective +4. Output a clear, concise task description string (one sentence) +5. Use the same language as the conversation +6. Focus on WHAT needs to be done, not HOW to do it +7. Do not include any explanations, just output the rewritten task string directly + +# Output +Please output only the rewritten task query string, without any additional formatting or explanation. +""" + + +TASK_QUERY_REWRITE_PROMPT_ZH = """ +# 角色 +你是理解用户意图和任务需求的专家。你擅长分析对话并提取核心任务描述。 + +# 任务 +基于提供的任务类型和对话消息,分析并确定用户想要完成的具体任务,然后将其重写为清晰、简洁的任务查询字符串。 + +# 任务类型 +{task_type} + +# 对话消息 +{messages} + +# 要求 +1. 分析对话内容以理解用户的核心意图 +2. 将任务类型作为上下文考虑 +3. 提取并总结关键任务目标 +4. 输出清晰、简洁的任务描述字符串(一句话) +5. 使用与对话相同的语言 +6. 关注需要做什么(WHAT),而不是如何做(HOW) +7. 不要包含任何解释,直接输出重写后的任务字符串 + +# 输出 +请仅输出重写后的任务查询字符串,不要添加任何额外的格式或解释。 +""" + +SKILLS_AUTHORING_PROMPT = """ +"""