mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-21 05:16:54 +08:00
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:
40
test/testcases/test_web_api/test_memory_app/conftest.py
Normal file
40
test/testcases/test_web_api/test_memory_app/conftest.py
Normal 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
|
||||
@ -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
|
||||
118
test/testcases/test_web_api/test_memory_app/test_list_memory.py
Normal file
118
test/testcases/test_web_api/test_memory_app/test_list_memory.py
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
Reference in New Issue
Block a user