Feat/memory (#11812)

### What problem does this PR solve?

Manage and display memory datasets.

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
Lynn
2025-12-10 13:34:08 +08:00
committed by GitHub
parent fd7e55b23d
commit a1164b9c89
13 changed files with 953 additions and 2 deletions

View File

@ -0,0 +1,40 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import pytest
import random
from test_web_api.common import create_memory, list_memory, delete_memory
@pytest.fixture(scope="function")
def add_memory_func(request, WebApiAuth):
def cleanup():
memory_list_res = list_memory(WebApiAuth)
exist_memory_ids = [memory["id"] for memory in memory_list_res["data"]["memory_list"]]
for memory_id in exist_memory_ids:
delete_memory(WebApiAuth, memory_id)
request.addfinalizer(cleanup)
memory_ids = []
for i in range(3):
payload = {
"name": f"test_memory_{i}",
"memory_type": ["raw"] + random.choices(["semantic", "episodic", "procedural"], k=random.randint(0, 3)),
"embd_id": "SILICONFLOW@BAAI/bge-large-zh-v1.5",
"llm_id": "ZHIPU-AI@glm-4-flash"
}
res = create_memory(WebApiAuth, payload)
memory_ids.append(res["data"]["id"])
return memory_ids

View File

@ -0,0 +1,106 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import random
import re
import pytest
from test_web_api.common import create_memory
from configs import INVALID_API_TOKEN
from libs.auth import RAGFlowWebApiAuth
from hypothesis import example, given, settings
from test.testcases.utils.hypothesis_utils import valid_names
class TestAuthorization:
@pytest.mark.p1
@pytest.mark.parametrize(
"invalid_auth, expected_code, expected_message",
[
(None, 401, "<Unauthorized '401: Unauthorized'>"),
(RAGFlowWebApiAuth(INVALID_API_TOKEN), 401, "<Unauthorized '401: Unauthorized'>"),
],
ids=["empty_auth", "invalid_api_token"]
)
def test_auth_invalid(self, invalid_auth, expected_code, expected_message):
res = create_memory(invalid_auth)
assert res["code"] == expected_code, res
assert res["message"] == expected_message, res
class TestMemoryCreate:
@pytest.mark.p1
@given(name=valid_names())
@example("d" * 128)
@settings(max_examples=20)
def test_name(self, WebApiAuth, name):
payload = {
"name": name,
"memory_type": ["raw"] + random.choices(["semantic", "episodic", "procedural"], k=random.randint(0, 3)),
"embd_id": "SILICONFLOW@BAAI/bge-large-zh-v1.5",
"llm_id": "ZHIPU-AI@glm-4-flash"
}
res = create_memory(WebApiAuth, payload)
assert res["code"] == 0, res
pattern = rf'^{name}|{name}(?:\((\d+)\))?$'
escaped_name = re.escape(res["data"]["name"])
assert re.match(pattern, escaped_name), res
@pytest.mark.p2
@pytest.mark.parametrize(
"name, expected_message",
[
("", "Memory name cannot be empty or whitespace."),
(" ", "Memory name cannot be empty or whitespace."),
("a" * 129, f"Memory name '{'a'*129}' exceeds limit of 128."),
],
ids=["empty_name", "space_name", "too_long_name"],
)
def test_name_invalid(self, WebApiAuth, name, expected_message):
payload = {
"name": name,
"memory_type": ["raw"] + random.choices(["semantic", "episodic", "procedural"], k=random.randint(0, 3)),
"embd_id": "SILICONFLOW@BAAI/bge-large-zh-v1.5",
"llm_id": "ZHIPU-AI@glm-4-flash"
}
res = create_memory(WebApiAuth, payload)
assert res["message"] == expected_message, res
@pytest.mark.p2
@given(name=valid_names())
def test_type_invalid(self, WebApiAuth, name):
payload = {
"name": name,
"memory_type": ["something"],
"embd_id": "SILICONFLOW@BAAI/bge-large-zh-v1.5",
"llm_id": "ZHIPU-AI@glm-4-flash"
}
res = create_memory(WebApiAuth, payload)
assert res["message"] == f"Memory type '{ {'something'} }' is not supported.", res
@pytest.mark.p3
def test_name_duplicated(self, WebApiAuth):
name = "duplicated_name_test"
payload = {
"name": name,
"memory_type": ["raw"] + random.choices(["semantic", "episodic", "procedural"], k=random.randint(0, 3)),
"embd_id": "SILICONFLOW@BAAI/bge-large-zh-v1.5",
"llm_id": "ZHIPU-AI@glm-4-flash"
}
res1 = create_memory(WebApiAuth, payload)
assert res1["code"] == 0, res1
res2 = create_memory(WebApiAuth, payload)
assert res2["code"] == 0, res2

View File

@ -0,0 +1,118 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from concurrent.futures import ThreadPoolExecutor, as_completed
import pytest
from test_web_api.common import list_memory, get_memory_config
from configs import INVALID_API_TOKEN
from libs.auth import RAGFlowWebApiAuth
class TestAuthorization:
@pytest.mark.p1
@pytest.mark.parametrize(
"invalid_auth, expected_code, expected_message",
[
(None, 401, "<Unauthorized '401: Unauthorized'>"),
(RAGFlowWebApiAuth(INVALID_API_TOKEN), 401, "<Unauthorized '401: Unauthorized'>"),
],
)
def test_auth_invalid(self, invalid_auth, expected_code, expected_message):
res = list_memory(invalid_auth)
assert res["code"] == expected_code, res
assert res["message"] == expected_message, res
class TestCapability:
@pytest.mark.p3
def test_capability(self, WebApiAuth):
count = 100
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(list_memory, WebApiAuth) for i in range(count)]
responses = list(as_completed(futures))
assert len(responses) == count, responses
assert all(future.result()["code"] == 0 for future in futures)
@pytest.mark.usefixtures("add_memory_func")
class TestMemoryList:
@pytest.mark.p1
def test_params_unset(self, WebApiAuth):
res = list_memory(WebApiAuth, None)
assert res["code"] == 0, res
@pytest.mark.p1
def test_params_empty(self, WebApiAuth):
res = list_memory(WebApiAuth, {})
assert res["code"] == 0, res
@pytest.mark.p1
@pytest.mark.parametrize(
"params, expected_page_size",
[
({"page": 1, "page_size": 10}, 3),
({"page": 2, "page_size": 10}, 0),
({"page": 1, "page_size": 2}, 2),
({"page": 2, "page_size": 2}, 1),
({"page": 5, "page_size": 10}, 0),
],
ids=["normal_first_page", "beyond_max_page", "normal_last_partial_page" , "normal_middle_page",
"full_data_single_page"],
)
def test_page(self, WebApiAuth, params, expected_page_size):
# have added 3 memories in fixture
res = list_memory(WebApiAuth, params)
assert res["code"] == 0, res
assert len(res["data"]["memory_list"]) == expected_page_size, res
@pytest.mark.p2
def test_filter_memory_type(self, WebApiAuth):
res = list_memory(WebApiAuth, {"memory_type": ["semantic"]})
assert res["code"] == 0, res
for memory in res["data"]["memory_list"]:
assert "semantic" in memory["memory_type"], res
@pytest.mark.p2
def test_filter_multi_memory_type(self, WebApiAuth):
res = list_memory(WebApiAuth, {"memory_type": ["episodic", "procedural"]})
assert res["code"] == 0, res
for memory in res["data"]["memory_list"]:
assert "episodic" in memory["memory_type"] or "procedural" in memory["memory_type"], res
@pytest.mark.p2
def test_filter_storage_type(self, WebApiAuth):
res = list_memory(WebApiAuth, {"storage_type": "table"})
assert res["code"] == 0, res
for memory in res["data"]["memory_list"]:
assert memory["storage_type"] == "table", res
@pytest.mark.p2
def test_match_keyword(self, WebApiAuth):
res = list_memory(WebApiAuth, {"keywords": "s"})
assert res["code"] == 0, res
for memory in res["data"]["memory_list"]:
assert "s" in memory["name"], res
@pytest.mark.p1
def test_get_config(self, WebApiAuth):
memory_list = list_memory(WebApiAuth, {})
assert memory_list["code"] == 0, memory_list
memory_config = get_memory_config(WebApiAuth, memory_list["data"]["memory_list"][0]["id"])
assert memory_config["code"] == 0, memory_config
assert memory_config["data"]["id"] == memory_list["data"]["memory_list"][0]["id"], memory_config
for field in ["name", "avatar", "tenant_id", "owner_name", "memory_type", "storage_type",
"embd_id", "llm_id", "permissions", "description", "memory_size", "forgetting_policy",
"temperature", "system_prompt", "user_prompt"]:
assert field in memory_config["data"], memory_config

View File

@ -0,0 +1,53 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import pytest
from test_web_api.common import (list_memory, delete_memory)
from configs import INVALID_API_TOKEN
from libs.auth import RAGFlowWebApiAuth
class TestAuthorization:
@pytest.mark.p1
@pytest.mark.parametrize(
"invalid_auth, expected_code, expected_message",
[
(None, 401, "<Unauthorized '401: Unauthorized'>"),
(RAGFlowWebApiAuth(INVALID_API_TOKEN), 401, "<Unauthorized '401: Unauthorized'>"),
],
)
def test_auth_invalid(self, invalid_auth, expected_code, expected_message):
res = delete_memory(invalid_auth, "some_memory_id")
assert res["code"] == expected_code, res
assert res["message"] == expected_message, res
class TestMemoryDelete:
@pytest.mark.p1
def test_memory_id(self, WebApiAuth, add_memory_func):
memory_ids = add_memory_func
res = delete_memory(WebApiAuth, memory_ids[0])
assert res["code"] == 0, res
res = list_memory(WebApiAuth)
assert res["data"]["total_count"] == 2, res
@pytest.mark.p2
@pytest.mark.usefixtures("add_memory_func")
def test_id_wrong_uuid(self, WebApiAuth):
res = delete_memory(WebApiAuth, "d94a8dc02c9711f0930f7fbc369eab6d")
assert res["code"] == 404, res
res = list_memory(WebApiAuth)
assert len(res["data"]["memory_list"]) == 3, res

View File

@ -0,0 +1,161 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import pytest
from test_web_api.common import update_memory
from configs import INVALID_API_TOKEN
from libs.auth import RAGFlowWebApiAuth
from hypothesis import HealthCheck, example, given, settings
from utils import encode_avatar
from utils.file_utils import create_image_file
from utils.hypothesis_utils import valid_names
class TestAuthorization:
@pytest.mark.p1
@pytest.mark.parametrize(
"invalid_auth, expected_code, expected_message",
[
(None, 401, "<Unauthorized '401: Unauthorized'>"),
(RAGFlowWebApiAuth(INVALID_API_TOKEN), 401, "<Unauthorized '401: Unauthorized'>"),
],
ids=["empty_auth", "invalid_api_token"]
)
def test_auth_invalid(self, invalid_auth, expected_code, expected_message):
res = update_memory(invalid_auth, "memory_id")
assert res["code"] == expected_code, res
assert res["message"] == expected_message, res
class TestMemoryUpdate:
@pytest.mark.p1
@given(name=valid_names())
@example("f" * 128)
@settings(max_examples=20, suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_name(self, WebApiAuth, add_memory_func, name):
memory_ids = add_memory_func
payload = {"name": name}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
assert res["data"]["name"] == name, res
@pytest.mark.p2
@pytest.mark.parametrize(
"name, expected_message",
[
("", "Memory name cannot be empty or whitespace."),
(" ", "Memory name cannot be empty or whitespace."),
("a" * 129, f"Memory name '{'a' * 129}' exceeds limit of 128."),
]
)
def test_name_invalid(self, WebApiAuth, add_memory_func, name, expected_message):
memory_ids = add_memory_func
payload = {"name": name}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 101, res
assert res["message"] == expected_message, res
@pytest.mark.p2
def test_duplicate_name(self, WebApiAuth, add_memory_func):
memory_ids = add_memory_func
payload = {"name": "Test_Memory"}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
payload = {"name": "Test_Memory"}
res = update_memory(WebApiAuth, memory_ids[1], payload)
assert res["code"] == 0, res
assert res["data"]["name"] == "Test_Memory(1)", res
@pytest.mark.p1
def test_avatar(self, WebApiAuth, add_memory_func, tmp_path):
memory_ids = add_memory_func
fn = create_image_file(tmp_path / "ragflow_test.png")
payload = {"avatar": f"data:image/png;base64,{encode_avatar(fn)}"}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
assert res["data"]["avatar"] == f"data:image/png;base64,{encode_avatar(fn)}", res
@pytest.mark.p1
def test_description(self, WebApiAuth, add_memory_func):
memory_ids = add_memory_func
description = "This is a test description."
payload = {"description": description}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
assert res["data"]["description"] == description, res
@pytest.mark.p1
def test_llm(self, WebApiAuth, add_memory_func):
memory_ids = add_memory_func
llm_id = "ZHIPU-AI@glm-4"
payload = {"llm_id": llm_id}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
assert res["data"]["llm_id"] == llm_id, res
@pytest.mark.p1
@pytest.mark.parametrize(
"permission",
[
"me",
"team"
],
ids=["me", "team"]
)
def test_permission(self, WebApiAuth, add_memory_func, permission):
memory_ids = add_memory_func
payload = {"permissions": permission}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
assert res["data"]["permissions"] == permission.lower().strip(), res
@pytest.mark.p1
def test_memory_size(self, WebApiAuth, add_memory_func):
memory_ids = add_memory_func
memory_size = 1048576 # 1 MB
payload = {"memory_size": memory_size}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
assert res["data"]["memory_size"] == memory_size, res
@pytest.mark.p1
def test_temperature(self, WebApiAuth, add_memory_func):
memory_ids = add_memory_func
temperature = 0.7
payload = {"temperature": temperature}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
assert res["data"]["temperature"] == temperature, res
@pytest.mark.p1
def test_system_prompt(self, WebApiAuth, add_memory_func):
memory_ids = add_memory_func
system_prompt = "This is a system prompt."
payload = {"system_prompt": system_prompt}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
assert res["data"]["system_prompt"] == system_prompt, res
@pytest.mark.p1
def test_user_prompt(self, WebApiAuth, add_memory_func):
memory_ids = add_memory_func
user_prompt = "This is a user prompt."
payload = {"user_prompt": user_prompt}
res = update_memory(WebApiAuth, memory_ids[0], payload)
assert res["code"] == 0, res
assert res["data"]["user_prompt"] == user_prompt, res