### What problem does this PR solve? This commit introduces a comprehensive test suite for the dialog app, including tests for creating, updating, retrieving, listing, and deleting dialogs. Additionally, the common.py file has been updated to include necessary API endpoints and helper functions for dialog operations. ### Type of change - [x] Add test casestags/v0.20.0
| @@ -25,7 +25,7 @@ HEADERS = {"Content-Type": "application/json"} | |||
| KB_APP_URL = f"/{VERSION}/kb" | |||
| DOCUMENT_APP_URL = f"/{VERSION}/document" | |||
| CHUNK_API_URL = f"/{VERSION}/chunk" | |||
| # CHAT_ASSISTANT_API_URL = "/api/v1/chats" | |||
| DIALOG_APP_URL = f"/{VERSION}/dialog" | |||
| # SESSION_WITH_CHAT_ASSISTANT_API_URL = "/api/v1/chats/{chat_id}/sessions" | |||
| # SESSION_WITH_AGENT_API_URL = "/api/v1/agents/{agent_id}/sessions" | |||
| @@ -201,3 +201,60 @@ def batch_add_chunks(auth, doc_id, num): | |||
| res = add_chunk(auth, {"doc_id": doc_id, "content_with_weight": f"chunk test {i}"}) | |||
| chunk_ids.append(res["data"]["chunk_id"]) | |||
| return chunk_ids | |||
| # DIALOG APP | |||
| def create_dialog(auth, payload=None, *, headers=HEADERS, data=None): | |||
| res = requests.post(url=f"{HOST_ADDRESS}{DIALOG_APP_URL}/set", headers=headers, auth=auth, json=payload, data=data) | |||
| return res.json() | |||
| def update_dialog(auth, payload=None, *, headers=HEADERS, data=None): | |||
| res = requests.post(url=f"{HOST_ADDRESS}{DIALOG_APP_URL}/set", headers=headers, auth=auth, json=payload, data=data) | |||
| return res.json() | |||
| def get_dialog(auth, params=None, *, headers=HEADERS): | |||
| res = requests.get(url=f"{HOST_ADDRESS}{DIALOG_APP_URL}/get", headers=headers, auth=auth, params=params) | |||
| return res.json() | |||
| def list_dialogs(auth, *, headers=HEADERS): | |||
| res = requests.get(url=f"{HOST_ADDRESS}{DIALOG_APP_URL}/list", headers=headers, auth=auth) | |||
| return res.json() | |||
| def delete_dialog(auth, payload=None, *, headers=HEADERS, data=None): | |||
| res = requests.post(url=f"{HOST_ADDRESS}{DIALOG_APP_URL}/rm", headers=headers, auth=auth, json=payload, data=data) | |||
| return res.json() | |||
| def batch_create_dialogs(auth, num, kb_ids=None): | |||
| if kb_ids is None: | |||
| kb_ids = [] | |||
| dialog_ids = [] | |||
| for i in range(num): | |||
| payload = { | |||
| "name": f"dialog_{i}", | |||
| "description": f"Test dialog {i}", | |||
| "kb_ids": kb_ids, | |||
| "prompt_config": {"system": "You are a helpful assistant. Use the following knowledge to answer questions: {knowledge}", "parameters": [{"key": "knowledge", "optional": False}]}, | |||
| "top_n": 6, | |||
| "top_k": 1024, | |||
| "similarity_threshold": 0.1, | |||
| "vector_similarity_weight": 0.3, | |||
| "llm_setting": {"model": "gpt-3.5-turbo", "temperature": 0.7}, | |||
| } | |||
| res = create_dialog(auth, payload) | |||
| if res["code"] == 0: | |||
| dialog_ids.append(res["data"]["id"]) | |||
| return dialog_ids | |||
| def delete_dialogs(auth): | |||
| res = list_dialogs(auth) | |||
| if res["code"] == 0 and res["data"]: | |||
| dialog_ids = [dialog["id"] for dialog in res["data"]] | |||
| if dialog_ids: | |||
| delete_dialog(auth, {"dialog_ids": dialog_ids}) | |||
| @@ -21,6 +21,7 @@ from common import ( | |||
| batch_create_datasets, | |||
| bulk_upload_documents, | |||
| delete_chunks, | |||
| delete_dialogs, | |||
| list_chunks, | |||
| list_documents, | |||
| list_kbs, | |||
| @@ -97,6 +98,14 @@ def clear_datasets(request: FixtureRequest, WebApiAuth: RAGFlowWebApiAuth): | |||
| request.addfinalizer(cleanup) | |||
| @pytest.fixture(scope="function") | |||
| def clear_dialogs(request, WebApiAuth): | |||
| def cleanup(): | |||
| delete_dialogs(WebApiAuth) | |||
| request.addfinalizer(cleanup) | |||
| @pytest.fixture(scope="class") | |||
| def add_dataset(request: FixtureRequest, WebApiAuth: RAGFlowWebApiAuth) -> str: | |||
| def cleanup(): | |||
| @@ -0,0 +1,50 @@ | |||
| # | |||
| # 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 common import batch_create_dialogs, delete_dialogs | |||
| @pytest.fixture(scope="function") | |||
| def add_dialog_func(request, WebApiAuth, add_dataset_func): | |||
| def cleanup(): | |||
| delete_dialogs(WebApiAuth) | |||
| request.addfinalizer(cleanup) | |||
| dataset_id = add_dataset_func | |||
| return dataset_id, batch_create_dialogs(WebApiAuth, 1, [dataset_id])[0] | |||
| @pytest.fixture(scope="class") | |||
| def add_dialogs(request, WebApiAuth, add_dataset): | |||
| def cleanup(): | |||
| delete_dialogs(WebApiAuth) | |||
| request.addfinalizer(cleanup) | |||
| dataset_id = add_dataset | |||
| return dataset_id, batch_create_dialogs(WebApiAuth, 5, [dataset_id]) | |||
| @pytest.fixture(scope="function") | |||
| def add_dialogs_func(request, WebApiAuth, add_dataset_func): | |||
| def cleanup(): | |||
| delete_dialogs(WebApiAuth) | |||
| request.addfinalizer(cleanup) | |||
| dataset_id = add_dataset_func | |||
| return dataset_id, batch_create_dialogs(WebApiAuth, 5, [dataset_id]) | |||
| @@ -0,0 +1,170 @@ | |||
| # | |||
| # 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 common import create_dialog | |||
| from configs import CHAT_ASSISTANT_NAME_LIMIT, INVALID_API_TOKEN | |||
| from hypothesis import example, given, settings | |||
| from libs.auth import RAGFlowWebApiAuth | |||
| from utils.hypothesis_utils import valid_names | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| 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): | |||
| payload = {"name": "auth_test", "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = create_dialog(invalid_auth, payload) | |||
| assert res["code"] == expected_code, res | |||
| assert res["message"] == expected_message, res | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| class TestCapability: | |||
| @pytest.mark.p3 | |||
| def test_create_dialog_100(self, WebApiAuth): | |||
| for i in range(100): | |||
| payload = {"name": f"dialog_{i}", "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, f"Failed to create dialog {i}" | |||
| @pytest.mark.p3 | |||
| def test_create_dialog_concurrent(self, WebApiAuth): | |||
| count = 100 | |||
| with ThreadPoolExecutor(max_workers=5) as executor: | |||
| futures = [executor.submit(create_dialog, WebApiAuth, {"name": f"dialog_{i}", "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}}) 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("clear_dialogs") | |||
| class TestDialogCreate: | |||
| @pytest.mark.p1 | |||
| @given(name=valid_names()) | |||
| @example("a" * CHAT_ASSISTANT_NAME_LIMIT) | |||
| @settings(max_examples=20) | |||
| def test_name(self, WebApiAuth, name): | |||
| payload = {"name": name, "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @pytest.mark.p2 | |||
| @pytest.mark.parametrize( | |||
| "name, expected_code, expected_message", | |||
| [ | |||
| ("", 102, "Dialog name can't be empty."), | |||
| (" ", 102, "Dialog name can't be empty."), | |||
| ("a" * (CHAT_ASSISTANT_NAME_LIMIT + 1), 102, "Dialog name length is 256 which is larger than 255"), | |||
| (0, 102, "Dialog name must be string."), | |||
| (None, 102, "Dialog name must be string."), | |||
| ], | |||
| ids=["empty_name", "space_name", "too_long_name", "invalid_name", "None_name"], | |||
| ) | |||
| def test_name_invalid(self, WebApiAuth, name, expected_code, expected_message): | |||
| payload = {"name": name, "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == expected_code, res | |||
| assert res["message"] == expected_message, res | |||
| @pytest.mark.p1 | |||
| def test_prompt_config_required(self, WebApiAuth): | |||
| payload = {"name": "test_dialog"} | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 101, res | |||
| assert res["message"] == "required argument are missing: prompt_config; ", res | |||
| @pytest.mark.p1 | |||
| def test_prompt_config_with_knowledge_no_kb(self, WebApiAuth): | |||
| payload = {"name": "test_dialog", "prompt_config": {"system": "You are a helpful assistant. Use this knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}} | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 102, res | |||
| assert "Please remove `{knowledge}` in system prompt" in res["message"], res | |||
| @pytest.mark.p1 | |||
| def test_prompt_config_parameter_not_used(self, WebApiAuth): | |||
| payload = {"name": "test_dialog", "prompt_config": {"system": "You are a helpful assistant.", "parameters": [{"key": "unused_param", "optional": False}]}} | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 102, res | |||
| assert "Parameter 'unused_param' is not used" in res["message"], res | |||
| @pytest.mark.p1 | |||
| def test_create_with_kb_ids(self, WebApiAuth, add_dataset_func): | |||
| dataset_id = add_dataset_func | |||
| payload = { | |||
| "name": "test_dialog_with_kb", | |||
| "kb_ids": [dataset_id], | |||
| "prompt_config": {"system": "You are a helpful assistant. Use this knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}, | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["kb_ids"] == [dataset_id], res | |||
| @pytest.mark.p2 | |||
| def test_create_with_all_parameters(self, WebApiAuth, add_dataset_func): | |||
| dataset_id = add_dataset_func | |||
| payload = { | |||
| "name": "comprehensive_dialog", | |||
| "description": "A comprehensive test dialog", | |||
| "icon": "🤖", | |||
| "kb_ids": [dataset_id], | |||
| "top_n": 10, | |||
| "top_k": 2048, | |||
| "rerank_id": "", | |||
| "similarity_threshold": 0.2, | |||
| "vector_similarity_weight": 0.5, | |||
| "llm_setting": {"model": "gpt-4", "temperature": 0.8, "max_tokens": 1000}, | |||
| "prompt_config": {"system": "You are a helpful assistant. Use this knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}, | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| data = res["data"] | |||
| assert data["name"] == "comprehensive_dialog", res | |||
| assert data["description"] == "A comprehensive test dialog", res | |||
| assert data["icon"] == "🤖", res | |||
| assert data["kb_ids"] == [dataset_id], res | |||
| assert data["top_n"] == 10, res | |||
| assert data["top_k"] == 2048, res | |||
| assert data["similarity_threshold"] == 0.2, res | |||
| assert data["vector_similarity_weight"] == 0.5, res | |||
| @pytest.mark.p3 | |||
| def test_name_duplicated(self, WebApiAuth): | |||
| name = "duplicated_dialog" | |||
| payload = {"name": name, "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @pytest.mark.p2 | |||
| def test_optional_parameters(self, WebApiAuth): | |||
| payload = { | |||
| "name": "test_optional_params", | |||
| "prompt_config": {"system": "You are a helpful assistant. Optional param: {optional_param}", "parameters": [{"key": "optional_param", "optional": True}]}, | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @@ -0,0 +1,204 @@ | |||
| # | |||
| # 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 common import batch_create_dialogs, create_dialog, delete_dialog, list_dialogs | |||
| from configs import INVALID_API_TOKEN | |||
| from libs.auth import RAGFlowWebApiAuth | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| 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, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| payload = {"dialog_ids": [dialog_id]} | |||
| res = delete_dialog(invalid_auth, payload) | |||
| assert res["code"] == expected_code, res | |||
| assert res["message"] == expected_message, res | |||
| class TestDialogDelete: | |||
| @pytest.mark.p1 | |||
| def test_delete_single_dialog(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 1, res | |||
| payload = {"dialog_ids": [dialog_id]} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"] is True, res | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 0, res | |||
| @pytest.mark.p1 | |||
| def test_delete_multiple_dialogs(self, WebApiAuth, add_dialogs_func): | |||
| _, dialog_ids = add_dialogs_func | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 5, res | |||
| payload = {"dialog_ids": dialog_ids} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"] is True, res | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 0, res | |||
| @pytest.mark.p1 | |||
| def test_delete_partial_dialogs(self, WebApiAuth, add_dialogs_func): | |||
| _, dialog_ids = add_dialogs_func | |||
| dialogs_to_delete = dialog_ids[:3] | |||
| payload = {"dialog_ids": dialogs_to_delete} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"] is True, res | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 2, res | |||
| remaining_ids = [dialog["id"] for dialog in res["data"]] | |||
| for dialog_id in dialog_ids[3:]: | |||
| assert dialog_id in remaining_ids, res | |||
| @pytest.mark.p2 | |||
| def test_delete_nonexistent_dialog(self, WebApiAuth): | |||
| fake_dialog_id = "nonexistent_dialog_id" | |||
| payload = {"dialog_ids": [fake_dialog_id]} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 103, res | |||
| assert "Only owner of dialog authorized for this operation." in res["message"], res | |||
| @pytest.mark.p2 | |||
| def test_delete_empty_dialog_ids(self, WebApiAuth): | |||
| payload = {"dialog_ids": []} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @pytest.mark.p2 | |||
| def test_delete_missing_dialog_ids(self, WebApiAuth): | |||
| payload = {} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 101, res | |||
| assert res["message"] == "required argument are missing: dialog_ids; ", res | |||
| @pytest.mark.p2 | |||
| def test_delete_invalid_dialog_ids_format(self, WebApiAuth): | |||
| payload = {"dialog_ids": "not_a_list"} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 103, res | |||
| assert res["message"] == "Only owner of dialog authorized for this operation.", res | |||
| @pytest.mark.p2 | |||
| def test_delete_mixed_valid_invalid_dialogs(self, WebApiAuth, add_dialog_func): | |||
| _, valid_dialog_id = add_dialog_func | |||
| invalid_dialog_id = "nonexistent_dialog_id" | |||
| payload = {"dialog_ids": [valid_dialog_id, invalid_dialog_id]} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 103, res | |||
| assert res["message"] == "Only owner of dialog authorized for this operation.", res | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 1, res | |||
| @pytest.mark.p3 | |||
| def test_delete_dialog_concurrent(self, WebApiAuth, add_dialogs_func): | |||
| _, dialog_ids = add_dialogs_func | |||
| count = len(dialog_ids) | |||
| with ThreadPoolExecutor(max_workers=3) as executor: | |||
| futures = [executor.submit(delete_dialog, WebApiAuth, {"dialog_ids": [dialog_id]}) for dialog_id in dialog_ids] | |||
| responses = [future.result() for future in as_completed(futures)] | |||
| successful_deletions = sum(1 for response in responses if response["code"] == 0) | |||
| assert successful_deletions > 0, "No dialogs were successfully deleted" | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == count - successful_deletions, res | |||
| @pytest.mark.p3 | |||
| def test_delete_dialog_idempotent(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| payload = {"dialog_ids": [dialog_id]} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @pytest.mark.p3 | |||
| def test_delete_large_batch_dialogs(self, WebApiAuth, add_document): | |||
| dataset_id, _ = add_document | |||
| dialog_ids = batch_create_dialogs(WebApiAuth, 50, [dataset_id]) | |||
| assert len(dialog_ids) == 50, "Failed to create 50 dialogs" | |||
| payload = {"dialog_ids": dialog_ids} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"] is True, res | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 0, res | |||
| @pytest.mark.p3 | |||
| def test_delete_dialog_with_special_characters(self, WebApiAuth): | |||
| payload = {"name": "Dialog with 特殊字符 and émojis 🤖", "description": "Test dialog with special characters", "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| create_res = create_dialog(WebApiAuth, payload) | |||
| assert create_res["code"] == 0, create_res | |||
| dialog_id = create_res["data"]["id"] | |||
| delete_payload = {"dialog_ids": [dialog_id]} | |||
| res = delete_dialog(WebApiAuth, delete_payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"] is True, res | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 0, res | |||
| @pytest.mark.p3 | |||
| def test_delete_dialog_preserves_other_user_dialogs(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| payload = {"dialog_ids": [dialog_id]} | |||
| res = delete_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @@ -0,0 +1,205 @@ | |||
| # | |||
| # 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 common import create_dialog, delete_dialog, get_dialog, update_dialog | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| class TestDialogEdgeCases: | |||
| @pytest.mark.p2 | |||
| def test_create_dialog_with_tavily_api_key(self, WebApiAuth): | |||
| """Test creating dialog with Tavily API key instead of knowledge base""" | |||
| payload = { | |||
| "name": "tavily_dialog", | |||
| "prompt_config": {"system": "You are a helpful assistant. Use this knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}], "tavily_api_key": "test_tavily_key"}, | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @pytest.mark.skip | |||
| @pytest.mark.p2 | |||
| def test_create_dialog_with_different_embedding_models(self, WebApiAuth): | |||
| """Test creating dialog with knowledge bases that have different embedding models""" | |||
| # This test would require creating datasets with different embedding models | |||
| # For now, we'll test the error case with a mock scenario | |||
| payload = { | |||
| "name": "mixed_embedding_dialog", | |||
| "kb_ids": ["kb_with_model_a", "kb_with_model_b"], | |||
| "prompt_config": {"system": "You are a helpful assistant with knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}, | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| # This should fail due to different embedding models | |||
| assert res["code"] == 102, res | |||
| assert "Datasets use different embedding models" in res["message"], res | |||
| @pytest.mark.p2 | |||
| def test_create_dialog_with_extremely_long_system_prompt(self, WebApiAuth): | |||
| """Test creating dialog with very long system prompt""" | |||
| long_prompt = "You are a helpful assistant. " * 1000 | |||
| payload = {"name": "long_prompt_dialog", "prompt_config": {"system": long_prompt, "parameters": []}} | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @pytest.mark.p2 | |||
| def test_create_dialog_with_unicode_characters(self, WebApiAuth): | |||
| """Test creating dialog with Unicode characters in various fields""" | |||
| payload = { | |||
| "name": "Unicode测试对话🤖", | |||
| "description": "测试Unicode字符支持 with émojis 🚀🌟", | |||
| "icon": "🤖", | |||
| "prompt_config": {"system": "你是一个有用的助手。You are helpful. Vous êtes utile. 🌍", "parameters": []}, | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["name"] == "Unicode测试对话🤖", res | |||
| assert res["data"]["description"] == "测试Unicode字符支持 with émojis 🚀🌟", res | |||
| @pytest.mark.p2 | |||
| def test_create_dialog_with_extreme_parameter_values(self, WebApiAuth): | |||
| """Test creating dialog with extreme parameter values""" | |||
| payload = { | |||
| "name": "extreme_params_dialog", | |||
| "top_n": 0, | |||
| "top_k": 1, | |||
| "similarity_threshold": 0.0, | |||
| "vector_similarity_weight": 1.0, | |||
| "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}, | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["top_n"] == 0, res | |||
| assert res["data"]["top_k"] == 1, res | |||
| assert res["data"]["similarity_threshold"] == 0.0, res | |||
| assert res["data"]["vector_similarity_weight"] == 1.0, res | |||
| @pytest.mark.p2 | |||
| def test_create_dialog_with_negative_parameter_values(self, WebApiAuth): | |||
| """Test creating dialog with negative parameter values""" | |||
| payload = { | |||
| "name": "negative_params_dialog", | |||
| "top_n": -1, | |||
| "top_k": -100, | |||
| "similarity_threshold": -0.5, | |||
| "vector_similarity_weight": -0.3, | |||
| "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}, | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] in [0, 102], res | |||
| @pytest.mark.p2 | |||
| def test_update_dialog_with_empty_kb_ids(self, WebApiAuth, add_dialog_func): | |||
| """Test updating dialog to remove all knowledge bases""" | |||
| dataset_id, dialog_id = add_dialog_func | |||
| payload = {"dialog_id": dialog_id, "kb_ids": [], "prompt_config": {"system": "You are a helpful assistant without knowledge.", "parameters": []}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["kb_ids"] == [], res | |||
| @pytest.mark.p2 | |||
| def test_update_dialog_with_null_values(self, WebApiAuth, add_dialog_func): | |||
| """Test updating dialog with null/None values""" | |||
| dataset_id, dialog_id = add_dialog_func | |||
| payload = {"dialog_id": dialog_id, "description": None, "icon": None, "rerank_id": None, "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @pytest.mark.p3 | |||
| def test_dialog_with_complex_prompt_parameters(self, WebApiAuth, add_dataset_func): | |||
| """Test dialog with complex prompt parameter configurations""" | |||
| payload = { | |||
| "name": "complex_params_dialog", | |||
| "prompt_config": { | |||
| "system": "You are {role} assistant. Use {knowledge} and consider {context}. Optional: {optional_param}", | |||
| "parameters": [{"key": "role", "optional": False}, {"key": "knowledge", "optional": True}, {"key": "context", "optional": False}, {"key": "optional_param", "optional": True}], | |||
| }, | |||
| "kb_ids": [add_dataset_func], | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @pytest.mark.p3 | |||
| def test_dialog_with_malformed_prompt_parameters(self, WebApiAuth): | |||
| """Test dialog with malformed prompt parameter configurations""" | |||
| payload = { | |||
| "name": "malformed_params_dialog", | |||
| "prompt_config": { | |||
| "system": "You are a helpful assistant.", | |||
| "parameters": [ | |||
| { | |||
| "key": "", | |||
| "optional": False, | |||
| }, | |||
| {"optional": True}, | |||
| { | |||
| "key": "valid_param", | |||
| }, | |||
| ], | |||
| }, | |||
| } | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] in [0, 102], res | |||
| @pytest.mark.p3 | |||
| def test_dialog_operations_with_special_ids(self, WebApiAuth): | |||
| """Test dialog operations with special ID formats""" | |||
| special_ids = [ | |||
| "00000000-0000-0000-0000-000000000000", | |||
| "ffffffff-ffff-ffff-ffff-ffffffffffff", | |||
| "12345678-1234-1234-1234-123456789abc", | |||
| ] | |||
| for special_id in special_ids: | |||
| res = get_dialog(WebApiAuth, {"dialog_id": special_id}) | |||
| assert res["code"] == 102, f"Should fail for ID: {special_id}" | |||
| res = delete_dialog(WebApiAuth, {"dialog_ids": [special_id]}) | |||
| assert res["code"] == 103, f"Should fail for ID: {special_id}" | |||
| @pytest.mark.p3 | |||
| def test_dialog_with_extremely_large_llm_settings(self, WebApiAuth): | |||
| """Test dialog with very large LLM settings""" | |||
| large_llm_setting = { | |||
| "model": "gpt-4", | |||
| "temperature": 0.7, | |||
| "max_tokens": 999999, | |||
| "custom_param_" + "x" * 1000: "large_value_" + "y" * 1000, | |||
| } | |||
| payload = {"name": "large_llm_settings_dialog", "llm_setting": large_llm_setting, "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = create_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| @pytest.mark.p3 | |||
| def test_concurrent_dialog_operations(self, WebApiAuth, add_dialog_func): | |||
| """Test concurrent operations on the same dialog""" | |||
| from concurrent.futures import ThreadPoolExecutor, as_completed | |||
| _, dialog_id = add_dialog_func | |||
| def update_operation(i): | |||
| payload = {"dialog_id": dialog_id, "name": f"concurrent_update_{i}", "prompt_config": {"system": f"You are assistant number {i}.", "parameters": []}} | |||
| return update_dialog(WebApiAuth, payload) | |||
| with ThreadPoolExecutor(max_workers=5) as executor: | |||
| futures = [executor.submit(update_operation, i) for i in range(10)] | |||
| responses = [future.result() for future in as_completed(futures)] | |||
| successful_updates = sum(1 for response in responses if response["code"] == 0) | |||
| assert successful_updates > 0, "No updates succeeded" | |||
| res = get_dialog(WebApiAuth, {"dialog_id": dialog_id}) | |||
| assert res["code"] == 0, res | |||
| @@ -0,0 +1,177 @@ | |||
| # | |||
| # 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 common import create_dialog, get_dialog | |||
| from configs import INVALID_API_TOKEN | |||
| from libs.auth import RAGFlowWebApiAuth | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| 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, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| res = get_dialog(invalid_auth, {"dialog_id": dialog_id}) | |||
| assert res["code"] == expected_code, res | |||
| assert res["message"] == expected_message, res | |||
| class TestDialogGet: | |||
| @pytest.mark.p1 | |||
| def test_get_existing_dialog(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| res = get_dialog(WebApiAuth, {"dialog_id": dialog_id}) | |||
| assert res["code"] == 0, res | |||
| data = res["data"] | |||
| assert data["id"] == dialog_id, res | |||
| assert "name" in data, res | |||
| assert "description" in data, res | |||
| assert "kb_ids" in data, res | |||
| assert "kb_names" in data, res | |||
| assert "prompt_config" in data, res | |||
| assert "llm_setting" in data, res | |||
| assert "top_n" in data, res | |||
| assert "top_k" in data, res | |||
| assert "similarity_threshold" in data, res | |||
| assert "vector_similarity_weight" in data, res | |||
| @pytest.mark.p1 | |||
| def test_get_dialog_with_kb_names(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| res = get_dialog(WebApiAuth, {"dialog_id": dialog_id}) | |||
| assert res["code"] == 0, res | |||
| data = res["data"] | |||
| assert isinstance(data["kb_ids"], list), res | |||
| assert isinstance(data["kb_names"], list), res | |||
| assert len(data["kb_ids"]) == len(data["kb_names"]), res | |||
| @pytest.mark.p2 | |||
| def test_get_nonexistent_dialog(self, WebApiAuth): | |||
| fake_dialog_id = "nonexistent_dialog_id" | |||
| res = get_dialog(WebApiAuth, {"dialog_id": fake_dialog_id}) | |||
| assert res["code"] == 102, res | |||
| assert "Dialog not found" in res["message"], res | |||
| @pytest.mark.p2 | |||
| def test_get_dialog_missing_id(self, WebApiAuth): | |||
| res = get_dialog(WebApiAuth, {}) | |||
| assert res["code"] == 100, res | |||
| assert res["message"] == "<BadRequestKeyError '400: Bad Request'>", res | |||
| @pytest.mark.p2 | |||
| def test_get_dialog_empty_id(self, WebApiAuth): | |||
| res = get_dialog(WebApiAuth, {"dialog_id": ""}) | |||
| assert res["code"] == 102, res | |||
| @pytest.mark.p2 | |||
| def test_get_dialog_invalid_id_format(self, WebApiAuth): | |||
| res = get_dialog(WebApiAuth, {"dialog_id": "invalid_format"}) | |||
| assert res["code"] == 102, res | |||
| @pytest.mark.p3 | |||
| def test_get_dialog_data_structure(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| res = get_dialog(WebApiAuth, {"dialog_id": dialog_id}) | |||
| assert res["code"] == 0, res | |||
| data = res["data"] | |||
| required_fields = [ | |||
| "id", | |||
| "name", | |||
| "description", | |||
| "kb_ids", | |||
| "kb_names", | |||
| "prompt_config", | |||
| "llm_setting", | |||
| "top_n", | |||
| "top_k", | |||
| "similarity_threshold", | |||
| "vector_similarity_weight", | |||
| "create_time", | |||
| "update_time", | |||
| ] | |||
| for field in required_fields: | |||
| assert field in data, f"Missing field: {field}" | |||
| assert isinstance(data["id"], str), res | |||
| assert isinstance(data["name"], str), res | |||
| assert isinstance(data["kb_ids"], list), res | |||
| assert isinstance(data["kb_names"], list), res | |||
| assert isinstance(data["prompt_config"], dict), res | |||
| assert isinstance(data["top_n"], int), res | |||
| assert isinstance(data["top_k"], int), res | |||
| assert isinstance(data["similarity_threshold"], (int, float)), res | |||
| assert isinstance(data["vector_similarity_weight"], (int, float)), res | |||
| @pytest.mark.p3 | |||
| def test_get_dialog_prompt_config_structure(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| res = get_dialog(WebApiAuth, {"dialog_id": dialog_id}) | |||
| assert res["code"] == 0, res | |||
| prompt_config = res["data"]["prompt_config"] | |||
| assert "system" in prompt_config, res | |||
| assert "parameters" in prompt_config, res | |||
| assert isinstance(prompt_config["system"], str), res | |||
| assert isinstance(prompt_config["parameters"], list), res | |||
| @pytest.mark.p3 | |||
| def test_get_dialog_with_multiple_kbs(self, WebApiAuth, add_dataset_func): | |||
| dataset_id1 = add_dataset_func | |||
| dataset_id2 = add_dataset_func | |||
| payload = { | |||
| "name": "multi_kb_dialog", | |||
| "kb_ids": [dataset_id1, dataset_id2], | |||
| "prompt_config": {"system": "You are a helpful assistant with knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}, | |||
| } | |||
| create_res = create_dialog(WebApiAuth, payload) | |||
| assert create_res["code"] == 0, create_res | |||
| dialog_id = create_res["data"]["id"] | |||
| res = get_dialog(WebApiAuth, {"dialog_id": dialog_id}) | |||
| assert res["code"] == 0, res | |||
| data = res["data"] | |||
| assert len(data["kb_ids"]) == 2, res | |||
| assert len(data["kb_names"]) == 2, res | |||
| assert dataset_id1 in data["kb_ids"], res | |||
| assert dataset_id2 in data["kb_ids"], res | |||
| @pytest.mark.p3 | |||
| def test_get_dialog_with_invalid_kb(self, WebApiAuth): | |||
| payload = { | |||
| "name": "invalid_kb_dialog", | |||
| "kb_ids": ["invalid_kb_id"], | |||
| "prompt_config": {"system": "You are a helpful assistant with knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}, | |||
| } | |||
| create_res = create_dialog(WebApiAuth, payload) | |||
| assert create_res["code"] == 0, create_res | |||
| dialog_id = create_res["data"]["id"] | |||
| res = get_dialog(WebApiAuth, {"dialog_id": dialog_id}) | |||
| assert res["code"] == 0, res | |||
| data = res["data"] | |||
| assert len(data["kb_ids"]) == 0, res | |||
| assert len(data["kb_names"]) == 0, res | |||
| @@ -0,0 +1,210 @@ | |||
| # | |||
| # 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 common import batch_create_dialogs, create_dialog, list_dialogs | |||
| from configs import INVALID_API_TOKEN | |||
| from libs.auth import RAGFlowWebApiAuth | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| 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 = list_dialogs(invalid_auth) | |||
| assert res["code"] == expected_code, res | |||
| assert res["message"] == expected_message, res | |||
| class TestDialogList: | |||
| @pytest.mark.p1 | |||
| @pytest.mark.usefixtures("add_dialogs_func") | |||
| def test_list_empty_dialogs(self, WebApiAuth): | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 5, res | |||
| @pytest.mark.p1 | |||
| def test_list_multiple_dialogs(self, WebApiAuth, add_dialogs_func): | |||
| _, dialog_ids = add_dialogs_func | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 5, res | |||
| returned_ids = [dialog["id"] for dialog in res["data"]] | |||
| for dialog_id in dialog_ids: | |||
| assert dialog_id in returned_ids, res | |||
| @pytest.mark.p2 | |||
| @pytest.mark.usefixtures("add_dialogs_func") | |||
| def test_list_dialogs_data_structure(self, WebApiAuth): | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 5, res | |||
| dialog = res["data"][0] | |||
| required_fields = [ | |||
| "id", | |||
| "name", | |||
| "description", | |||
| "kb_ids", | |||
| "kb_names", | |||
| "prompt_config", | |||
| "llm_setting", | |||
| "top_n", | |||
| "top_k", | |||
| "similarity_threshold", | |||
| "vector_similarity_weight", | |||
| "create_time", | |||
| "update_time", | |||
| ] | |||
| for field in required_fields: | |||
| assert field in dialog, f"Missing field: {field}" | |||
| assert isinstance(dialog["id"], str), res | |||
| assert isinstance(dialog["name"], str), res | |||
| assert isinstance(dialog["kb_ids"], list), res | |||
| assert isinstance(dialog["kb_names"], list), res | |||
| assert isinstance(dialog["prompt_config"], dict), res | |||
| assert isinstance(dialog["top_n"], int), res | |||
| assert isinstance(dialog["top_k"], int), res | |||
| assert isinstance(dialog["similarity_threshold"], (int, float)), res | |||
| assert isinstance(dialog["vector_similarity_weight"], (int, float)), res | |||
| @pytest.mark.p2 | |||
| @pytest.mark.usefixtures("add_dialogs_func") | |||
| def test_list_dialogs_with_kb_names(self, WebApiAuth): | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| dialog = res["data"][0] | |||
| assert isinstance(dialog["kb_ids"], list), res | |||
| assert isinstance(dialog["kb_names"], list), res | |||
| assert len(dialog["kb_ids"]) == len(dialog["kb_names"]), res | |||
| @pytest.mark.p2 | |||
| @pytest.mark.usefixtures("add_dialogs_func") | |||
| def test_list_dialogs_ordering(self, WebApiAuth): | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 5, res | |||
| dialogs = res["data"] | |||
| for i in range(len(dialogs) - 1): | |||
| current_time = dialogs[i]["create_time"] | |||
| next_time = dialogs[i + 1]["create_time"] | |||
| assert current_time >= next_time, f"Dialogs not properly ordered: {current_time} should be >= {next_time}" | |||
| @pytest.mark.p3 | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| def test_list_dialogs_with_invalid_kb(self, WebApiAuth): | |||
| payload = { | |||
| "name": "invalid_kb_dialog", | |||
| "kb_ids": ["invalid_kb_id"], | |||
| "prompt_config": {"system": "You are a helpful assistant with knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}, | |||
| } | |||
| create_res = create_dialog(WebApiAuth, payload) | |||
| assert create_res["code"] == 0, create_res | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 1, res | |||
| dialog = res["data"][0] | |||
| assert len(dialog["kb_ids"]) == 0, res | |||
| assert len(dialog["kb_names"]) == 0, res | |||
| @pytest.mark.p3 | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| def test_list_dialogs_with_multiple_kbs(self, WebApiAuth, add_dataset_func): | |||
| dataset_id1 = add_dataset_func | |||
| dataset_id2 = add_dataset_func | |||
| payload = { | |||
| "name": "multi_kb_dialog", | |||
| "kb_ids": [dataset_id1, dataset_id2], | |||
| "prompt_config": {"system": "You are a helpful assistant with knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}, | |||
| } | |||
| create_res = create_dialog(WebApiAuth, payload) | |||
| assert create_res["code"] == 0, create_res | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 1, res | |||
| dialog = res["data"][0] | |||
| assert len(dialog["kb_ids"]) == 2, res | |||
| assert len(dialog["kb_names"]) == 2, res | |||
| assert dataset_id1 in dialog["kb_ids"], res | |||
| assert dataset_id2 in dialog["kb_ids"], res | |||
| @pytest.mark.p3 | |||
| @pytest.mark.usefixtures("add_dialogs_func") | |||
| def test_list_dialogs_prompt_config_structure(self, WebApiAuth): | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| dialog = res["data"][0] | |||
| prompt_config = dialog["prompt_config"] | |||
| assert "system" in prompt_config, res | |||
| assert "parameters" in prompt_config, res | |||
| assert isinstance(prompt_config["system"], str), res | |||
| assert isinstance(prompt_config["parameters"], list), res | |||
| @pytest.mark.p3 | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| def test_list_dialogs_performance(self, WebApiAuth, add_document): | |||
| dataset_id, _ = add_document | |||
| dialog_ids = batch_create_dialogs(WebApiAuth, 100, [dataset_id]) | |||
| assert len(dialog_ids) == 100, "Failed to create 100 dialogs" | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 100, res | |||
| returned_ids = [dialog["id"] for dialog in res["data"]] | |||
| for dialog_id in dialog_ids: | |||
| assert dialog_id in returned_ids, f"Dialog {dialog_id} not found in list" | |||
| @pytest.mark.p3 | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| def test_list_dialogs_with_mixed_kb_states(self, WebApiAuth, add_dataset_func): | |||
| valid_dataset_id = add_dataset_func | |||
| payload = { | |||
| "name": "mixed_kb_dialog", | |||
| "kb_ids": [valid_dataset_id, "invalid_kb_id"], | |||
| "prompt_config": {"system": "You are a helpful assistant with knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}, | |||
| } | |||
| create_res = create_dialog(WebApiAuth, payload) | |||
| assert create_res["code"] == 0, create_res | |||
| res = list_dialogs(WebApiAuth) | |||
| assert res["code"] == 0, res | |||
| assert len(res["data"]) == 1, res | |||
| dialog = res["data"][0] | |||
| assert len(dialog["kb_ids"]) == 1, res | |||
| assert dialog["kb_ids"][0] == valid_dataset_id, res | |||
| assert len(dialog["kb_names"]) == 1, res | |||
| @@ -0,0 +1,170 @@ | |||
| # | |||
| # 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 common import update_dialog | |||
| from configs import INVALID_API_TOKEN | |||
| from libs.auth import RAGFlowWebApiAuth | |||
| @pytest.mark.usefixtures("clear_dialogs") | |||
| 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, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| payload = {"dialog_id": dialog_id, "name": "updated_name", "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = update_dialog(invalid_auth, payload) | |||
| assert res["code"] == expected_code, res | |||
| assert res["message"] == expected_message, res | |||
| class TestDialogUpdate: | |||
| @pytest.mark.p1 | |||
| def test_update_name(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| new_name = "updated_dialog_name" | |||
| payload = {"dialog_id": dialog_id, "name": new_name, "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["name"] == new_name, res | |||
| @pytest.mark.p1 | |||
| def test_update_description(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| new_description = "Updated description" | |||
| payload = {"dialog_id": dialog_id, "description": new_description, "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["description"] == new_description, res | |||
| @pytest.mark.p1 | |||
| def test_update_prompt_config(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| new_prompt_config = {"system": "You are an updated helpful assistant with {param1}.", "parameters": [{"key": "param1", "optional": False}]} | |||
| payload = {"dialog_id": dialog_id, "prompt_config": new_prompt_config} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["prompt_config"]["system"] == new_prompt_config["system"], res | |||
| @pytest.mark.p1 | |||
| def test_update_kb_ids(self, WebApiAuth, add_dialog_func, add_dataset_func): | |||
| _, dialog_id = add_dialog_func | |||
| new_dataset_id = add_dataset_func | |||
| payload = { | |||
| "dialog_id": dialog_id, | |||
| "kb_ids": [new_dataset_id], | |||
| "prompt_config": {"system": "You are a helpful assistant with knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}, | |||
| } | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert new_dataset_id in res["data"]["kb_ids"], res | |||
| @pytest.mark.p1 | |||
| def test_update_llm_settings(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| new_llm_setting = {"model": "gpt-4", "temperature": 0.9, "max_tokens": 2000} | |||
| payload = {"dialog_id": dialog_id, "llm_setting": new_llm_setting, "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["llm_setting"]["model"] == "gpt-4", res | |||
| assert res["data"]["llm_setting"]["temperature"] == 0.9, res | |||
| @pytest.mark.p1 | |||
| def test_update_retrieval_settings(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| payload = { | |||
| "dialog_id": dialog_id, | |||
| "top_n": 15, | |||
| "top_k": 4096, | |||
| "similarity_threshold": 0.3, | |||
| "vector_similarity_weight": 0.7, | |||
| "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}, | |||
| } | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["top_n"] == 15, res | |||
| assert res["data"]["top_k"] == 4096, res | |||
| assert res["data"]["similarity_threshold"] == 0.3, res | |||
| assert res["data"]["vector_similarity_weight"] == 0.7, res | |||
| @pytest.mark.p2 | |||
| def test_update_nonexistent_dialog(self, WebApiAuth): | |||
| fake_dialog_id = "nonexistent_dialog_id" | |||
| payload = {"dialog_id": fake_dialog_id, "name": "updated_name", "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 102, res | |||
| assert "Dialog not found" in res["message"], res | |||
| @pytest.mark.p2 | |||
| def test_update_with_invalid_prompt_config(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| payload = {"dialog_id": dialog_id, "prompt_config": {"system": "You are a helpful assistant.", "parameters": [{"key": "unused_param", "optional": False}]}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 102, res | |||
| assert "Parameter 'unused_param' is not used" in res["message"], res | |||
| @pytest.mark.p2 | |||
| def test_update_with_knowledge_but_no_kb(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| payload = {"dialog_id": dialog_id, "kb_ids": [], "prompt_config": {"system": "You are a helpful assistant with knowledge: {knowledge}", "parameters": [{"key": "knowledge", "optional": True}]}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 102, res | |||
| assert "Please remove `{knowledge}` in system prompt" in res["message"], res | |||
| @pytest.mark.p2 | |||
| def test_update_icon(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| new_icon = "🚀" | |||
| payload = {"dialog_id": dialog_id, "icon": new_icon, "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["icon"] == new_icon, res | |||
| @pytest.mark.p2 | |||
| def test_update_rerank_id(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| payload = {"dialog_id": dialog_id, "rerank_id": "test_rerank_model", "prompt_config": {"system": "You are a helpful assistant.", "parameters": []}} | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| assert res["data"]["rerank_id"] == "test_rerank_model", res | |||
| @pytest.mark.p3 | |||
| def test_update_multiple_fields(self, WebApiAuth, add_dialog_func): | |||
| _, dialog_id = add_dialog_func | |||
| payload = { | |||
| "dialog_id": dialog_id, | |||
| "name": "multi_update_dialog", | |||
| "description": "Updated with multiple fields", | |||
| "icon": "🔄", | |||
| "top_n": 20, | |||
| "similarity_threshold": 0.4, | |||
| "prompt_config": {"system": "You are a multi-updated assistant.", "parameters": []}, | |||
| } | |||
| res = update_dialog(WebApiAuth, payload) | |||
| assert res["code"] == 0, res | |||
| data = res["data"] | |||
| assert data["name"] == "multi_update_dialog", res | |||
| assert data["description"] == "Updated with multiple fields", res | |||
| assert data["icon"] == "🔄", res | |||
| assert data["top_n"] == 20, res | |||
| assert data["similarity_threshold"] == 0.4, res | |||