mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-19 14:11:04 +08:00
### What problem does this PR solve?
This reverts commit 1a608ac411.
### Type of change
- [x] Other (please describe):
This commit is contained in:
@@ -30,6 +30,7 @@ KB_APP_URL = f"/{VERSION}/kb"
|
||||
DATASETS_URL = f"/api/{VERSION}/datasets"
|
||||
DOCUMENT_APP_URL = f"/{VERSION}/document"
|
||||
CHUNK_API_URL = f"/{VERSION}/chunk"
|
||||
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"
|
||||
MEMORY_API_URL = f"/api/{VERSION}/memories"
|
||||
@@ -468,6 +469,103 @@ def batch_add_chunks(auth, doc_id, num):
|
||||
return chunk_ids
|
||||
|
||||
|
||||
# DIALOG APP
|
||||
def create_dialog(auth, payload=None, *, headers=HEADERS, data=None):
|
||||
if payload is None:
|
||||
payload = {}
|
||||
url = f"{HOST_ADDRESS}{DIALOG_APP_URL}/set"
|
||||
req_id = str(uuid.uuid4())
|
||||
req_headers = dict(headers)
|
||||
req_headers["X-Request-ID"] = req_id
|
||||
start = time.monotonic()
|
||||
res = requests.post(url=url, headers=req_headers, auth=auth, json=payload, data=data)
|
||||
elapsed_ms = (time.monotonic() - start) * 1000
|
||||
resp_json = None
|
||||
json_error = None
|
||||
try:
|
||||
resp_json = res.json()
|
||||
except ValueError as exc:
|
||||
json_error = exc
|
||||
_log_http_debug("POST", url, req_id, payload, res.status_code, res.text, resp_json, elapsed_ms)
|
||||
if _http_debug_enabled():
|
||||
if not res.ok or (resp_json is not None and resp_json.get("code") != 0):
|
||||
payload_summary = _redact_payload(payload)
|
||||
raise AssertionError(
|
||||
"HTTP helper failure: "
|
||||
f"req_id={req_id} url={url} status={res.status_code} "
|
||||
f"payload={payload_summary} response={res.text}"
|
||||
)
|
||||
if json_error:
|
||||
raise json_error
|
||||
return resp_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):
|
||||
if kb_ids:
|
||||
prompt_config = {
|
||||
"system": "You are a helpful assistant. Use the following knowledge to answer questions: {knowledge}",
|
||||
"parameters": [{"key": "knowledge", "optional": False}],
|
||||
}
|
||||
else:
|
||||
prompt_config = {
|
||||
"system": "You are a helpful assistant.",
|
||||
"parameters": [],
|
||||
}
|
||||
payload = {
|
||||
"name": f"dialog_{i}",
|
||||
"description": f"Test dialog {i}",
|
||||
"kb_ids": kb_ids,
|
||||
"prompt_config": prompt_config,
|
||||
"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 is None or res.get("code") != 0:
|
||||
uses_knowledge = "{knowledge}" in payload["prompt_config"]["system"]
|
||||
raise AssertionError(
|
||||
"batch_create_dialogs failed: "
|
||||
f"res={res} kb_ids_len={len(kb_ids)} uses_knowledge={uses_knowledge}"
|
||||
)
|
||||
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})
|
||||
|
||||
# MEMORY APP
|
||||
def create_memory(auth, payload=None):
|
||||
url = f"{HOST_ADDRESS}{MEMORY_API_URL}"
|
||||
|
||||
50
test/testcases/test_web_api/test_dialog_app/conftest.py
Normal file
50
test/testcases/test_web_api/test_dialog_app/conftest.py
Normal file
@@ -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 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
|
||||
|
||||
from common import create_dialog
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("clear_dialogs")
|
||||
class TestAuthorization:
|
||||
@pytest.mark.p2
|
||||
@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"] == 0, 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.p2
|
||||
@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 dataset"""
|
||||
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,572 @@
|
||||
#
|
||||
# Copyright 2026 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 asyncio
|
||||
import importlib.util
|
||||
import inspect
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from functools import wraps
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class _DummyManager:
|
||||
def route(self, *_args, **_kwargs):
|
||||
def decorator(func):
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class _AwaitableValue:
|
||||
def __init__(self, value):
|
||||
self._value = value
|
||||
|
||||
def __await__(self):
|
||||
async def _co():
|
||||
return self._value
|
||||
|
||||
return _co().__await__()
|
||||
|
||||
|
||||
class _Args(dict):
|
||||
def get(self, key, default=None):
|
||||
return super().get(key, default)
|
||||
|
||||
|
||||
def _run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def _set_request_json(monkeypatch, module, payload):
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue(payload))
|
||||
|
||||
|
||||
def _set_request_args(monkeypatch, module, args):
|
||||
monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args(args)))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def auth():
|
||||
return "unit-auth"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def set_tenant_info():
|
||||
return None
|
||||
|
||||
|
||||
def _load_dialog_module(monkeypatch):
|
||||
repo_root = Path(__file__).resolve().parents[4]
|
||||
|
||||
common_pkg = ModuleType("common")
|
||||
common_pkg.__path__ = [str(repo_root / "common")]
|
||||
monkeypatch.setitem(sys.modules, "common", common_pkg)
|
||||
|
||||
quart_mod = ModuleType("quart")
|
||||
quart_mod.request = SimpleNamespace(args=_Args())
|
||||
monkeypatch.setitem(sys.modules, "quart", quart_mod)
|
||||
|
||||
api_pkg = ModuleType("api")
|
||||
api_pkg.__path__ = [str(repo_root / "api")]
|
||||
monkeypatch.setitem(sys.modules, "api", api_pkg)
|
||||
|
||||
apps_mod = ModuleType("api.apps")
|
||||
apps_mod.__path__ = [str(repo_root / "api" / "apps")]
|
||||
apps_mod.current_user = SimpleNamespace(id="tenant-1")
|
||||
apps_mod.login_required = lambda func: func
|
||||
monkeypatch.setitem(sys.modules, "api.apps", apps_mod)
|
||||
api_pkg.apps = apps_mod
|
||||
|
||||
db_pkg = ModuleType("api.db")
|
||||
db_pkg.__path__ = []
|
||||
monkeypatch.setitem(sys.modules, "api.db", db_pkg)
|
||||
api_pkg.db = db_pkg
|
||||
|
||||
services_pkg = ModuleType("api.db.services")
|
||||
services_pkg.__path__ = []
|
||||
services_pkg.duplicate_name = lambda _checker, **kwargs: kwargs.get("name", "")
|
||||
monkeypatch.setitem(sys.modules, "api.db.services", services_pkg)
|
||||
|
||||
dialog_service_mod = ModuleType("api.db.services.dialog_service")
|
||||
|
||||
class _DialogService:
|
||||
model = SimpleNamespace(create_time="create_time")
|
||||
|
||||
@staticmethod
|
||||
def query(**_kwargs):
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def save(**_kwargs):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(*_args, **_kwargs):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(_id):
|
||||
return True, SimpleNamespace(to_dict=lambda: {"id": _id, "kb_ids": []})
|
||||
|
||||
@staticmethod
|
||||
def get_by_tenant_ids(*_args, **_kwargs):
|
||||
return [], 0
|
||||
|
||||
@staticmethod
|
||||
def update_many_by_id(_payload):
|
||||
return True
|
||||
|
||||
dialog_service_mod.DialogService = _DialogService
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.dialog_service", dialog_service_mod)
|
||||
|
||||
tenant_llm_service_mod = ModuleType("api.db.services.tenant_llm_service")
|
||||
|
||||
class _MockTableObject:
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def to_dict(self):
|
||||
return {k: v for k, v in self.__dict__.items()}
|
||||
|
||||
class _TenantLLMService:
|
||||
@staticmethod
|
||||
def split_model_name_and_factory(embd_id):
|
||||
return embd_id.split("@")
|
||||
|
||||
@staticmethod
|
||||
def get_api_key(tenant_id, model_name, model_type=None):
|
||||
return _MockTableObject(
|
||||
id=1,
|
||||
tenant_id=tenant_id,
|
||||
llm_factory="",
|
||||
model_type="chat",
|
||||
llm_name=model_name,
|
||||
api_key="fake-api-key",
|
||||
api_base="https://api.example.com",
|
||||
max_tokens=8192,
|
||||
used_tokens=0,
|
||||
status=1
|
||||
)
|
||||
|
||||
tenant_llm_service_mod.TenantLLMService = _TenantLLMService
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.tenant_llm_service", tenant_llm_service_mod)
|
||||
|
||||
knowledgebase_service_mod = ModuleType("api.db.services.knowledgebase_service")
|
||||
|
||||
class _KnowledgebaseService:
|
||||
@staticmethod
|
||||
def get_by_ids(_ids):
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(_id):
|
||||
return False, None
|
||||
|
||||
@staticmethod
|
||||
def query(**_kwargs):
|
||||
return []
|
||||
|
||||
knowledgebase_service_mod.KnowledgebaseService = _KnowledgebaseService
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.knowledgebase_service", knowledgebase_service_mod)
|
||||
|
||||
user_service_mod = ModuleType("api.db.services.user_service")
|
||||
|
||||
class _TenantService:
|
||||
@staticmethod
|
||||
def get_by_id(_id):
|
||||
return True, SimpleNamespace(llm_id="llm-default")
|
||||
|
||||
class _UserTenantService:
|
||||
@staticmethod
|
||||
def query(**_kwargs):
|
||||
return [SimpleNamespace(tenant_id="tenant-1")]
|
||||
|
||||
user_service_mod.TenantService = _TenantService
|
||||
user_service_mod.UserTenantService = _UserTenantService
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.user_service", user_service_mod)
|
||||
|
||||
api_utils_mod = ModuleType("api.utils.api_utils")
|
||||
from common.constants import RetCode
|
||||
|
||||
async def _default_request_json():
|
||||
return {}
|
||||
|
||||
def _get_data_error_result(code=RetCode.DATA_ERROR, message="Sorry! Data missing!"):
|
||||
return {"code": code, "message": message}
|
||||
|
||||
def _get_json_result(code=RetCode.SUCCESS, message="success", data=None):
|
||||
return {"code": code, "message": message, "data": data}
|
||||
|
||||
def _server_error_response(error):
|
||||
return {"code": RetCode.EXCEPTION_ERROR, "message": repr(error)}
|
||||
|
||||
def _validate_request(*_args, **_kwargs):
|
||||
def _decorator(func):
|
||||
if inspect.iscoroutinefunction(func):
|
||||
@wraps(func)
|
||||
async def _wrapped(*func_args, **func_kwargs):
|
||||
return await func(*func_args, **func_kwargs)
|
||||
|
||||
return _wrapped
|
||||
|
||||
@wraps(func)
|
||||
def _wrapped(*func_args, **func_kwargs):
|
||||
return func(*func_args, **func_kwargs)
|
||||
|
||||
return _wrapped
|
||||
|
||||
return _decorator
|
||||
|
||||
api_utils_mod.get_request_json = _default_request_json
|
||||
api_utils_mod.get_data_error_result = _get_data_error_result
|
||||
api_utils_mod.get_json_result = _get_json_result
|
||||
api_utils_mod.server_error_response = _server_error_response
|
||||
api_utils_mod.validate_request = _validate_request
|
||||
monkeypatch.setitem(sys.modules, "api.utils.api_utils", api_utils_mod)
|
||||
|
||||
module_name = "test_dialog_routes_unit_module"
|
||||
module_path = repo_root / "api" / "apps" / "dialog_app.py"
|
||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
module.manager = _DummyManager()
|
||||
monkeypatch.setitem(sys.modules, module_name, module)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_set_dialog_branch_matrix_unit(monkeypatch):
|
||||
module = _load_dialog_module(monkeypatch)
|
||||
handler = inspect.unwrap(module.set_dialog)
|
||||
|
||||
_set_request_json(monkeypatch, module, {"name": 1, "prompt_config": {"system": "", "parameters": []}})
|
||||
res = _run(handler())
|
||||
assert res["message"] == "Dialog name must be string."
|
||||
|
||||
_set_request_json(monkeypatch, module, {"name": " ", "prompt_config": {"system": "", "parameters": []}})
|
||||
res = _run(handler())
|
||||
assert res["message"] == "Dialog name can't be empty."
|
||||
|
||||
_set_request_json(monkeypatch, module, {"name": "a" * 256, "prompt_config": {"system": "", "parameters": []}})
|
||||
res = _run(handler())
|
||||
assert res["message"] == "Dialog name length is 256 which is larger than 255"
|
||||
|
||||
captured = {}
|
||||
|
||||
def _dup_name(checker, **kwargs):
|
||||
assert checker(name=kwargs["name"]) is True
|
||||
return kwargs["name"] + " (1)"
|
||||
|
||||
monkeypatch.setattr(module, "duplicate_name", _dup_name)
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(name="new dialog")])
|
||||
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _id: (True, SimpleNamespace(llm_id="llm-x", tenant_llm_id=1)))
|
||||
monkeypatch.setattr(module.KnowledgebaseService, "get_by_ids", lambda _ids: [SimpleNamespace(embd_id="embd-a@builtin", tenant_embd_id=2)])
|
||||
monkeypatch.setattr(module.TenantLLMService, "split_model_name_and_factory", lambda embd_id: embd_id.split("@"))
|
||||
monkeypatch.setattr(module.DialogService, "save", lambda **kwargs: captured.update(kwargs) or False)
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"name": "New Dialog",
|
||||
"kb_ids": ["kb-1"],
|
||||
"prompt_config": {"system": "Use {knowledge}", "parameters": []},
|
||||
},
|
||||
)
|
||||
res = _run(handler())
|
||||
assert res["message"] == "Fail to new a dialog!"
|
||||
assert captured["name"] == "New Dialog (1)"
|
||||
assert captured["prompt_config"]["parameters"] == [{"key": "knowledge", "optional": False}]
|
||||
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"dialog_id": "dialog-1",
|
||||
"name": "Update",
|
||||
"kb_ids": [],
|
||||
"prompt_config": {
|
||||
"system": "Use {knowledge}",
|
||||
"parameters": [{"key": "knowledge", "optional": True}],
|
||||
},
|
||||
},
|
||||
)
|
||||
res = _run(handler())
|
||||
assert "Please remove `{knowledge}` in system prompt" in res["message"]
|
||||
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{"name": "demo", "prompt_config": {"system": "hello", "parameters": [{"key": "must", "optional": False}]}},
|
||||
)
|
||||
res = _run(handler())
|
||||
assert "Parameter 'must' is not used" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _id: (False, None))
|
||||
_set_request_json(monkeypatch, module, {"name": "demo", "prompt_config": {"system": "hello", "parameters": []}})
|
||||
res = _run(handler())
|
||||
assert res["message"] == "Tenant not found!"
|
||||
|
||||
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _id: (True, SimpleNamespace(llm_id="llm-x", tenant_llm_id=1)))
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"get_request_json",
|
||||
lambda: _AwaitableValue(
|
||||
{
|
||||
"name": "demo",
|
||||
"kb_ids": ["kb-1", "kb-2"],
|
||||
"prompt_config": {"system": "hello", "parameters": []},
|
||||
}
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
module.KnowledgebaseService,
|
||||
"get_by_ids",
|
||||
lambda _ids: [SimpleNamespace(embd_id="embd-a@f1", tenant_embd_id=2), SimpleNamespace(embd_id="embd-b@f2", tenant_embd_id=2)],
|
||||
)
|
||||
monkeypatch.setattr(module.TenantLLMService, "split_model_name_and_factory", lambda embd_id: embd_id.split("@"))
|
||||
res = _run(handler())
|
||||
assert "Datasets use different embedding models" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"get_request_json",
|
||||
lambda: _AwaitableValue(
|
||||
{
|
||||
"name": "optional-param-dialog",
|
||||
"prompt_config": {"system": "hello", "parameters": [{"key": "ignored", "optional": True}]},
|
||||
}
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(module.KnowledgebaseService, "get_by_ids", lambda _ids: [])
|
||||
monkeypatch.setattr(module.DialogService, "save", lambda **_kwargs: False)
|
||||
res = _run(handler())
|
||||
assert res["message"] == "Fail to new a dialog!"
|
||||
|
||||
monkeypatch.setattr(module.KnowledgebaseService, "get_by_ids", lambda _ids: [])
|
||||
monkeypatch.setattr(module.DialogService, "update_by_id", lambda *_args, **_kwargs: False)
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"dialog_id": "dialog-1",
|
||||
"kb_names": ["legacy"],
|
||||
"name": "rename",
|
||||
"prompt_config": {"system": "hello", "parameters": []},
|
||||
},
|
||||
)
|
||||
res = _run(handler())
|
||||
assert res["message"] == "Dialog not found!"
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "update_by_id", lambda *_args, **_kwargs: True)
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (False, None))
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"dialog_id": "dialog-1",
|
||||
"name": "rename",
|
||||
"prompt_config": {"system": "hello", "parameters": []},
|
||||
},
|
||||
)
|
||||
res = _run(handler())
|
||||
assert res["message"] == "Fail to update a dialog!"
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (True, SimpleNamespace(to_dict=lambda: {"id": _id, "kb_ids": ["kb-1"]})))
|
||||
monkeypatch.setattr(
|
||||
module.KnowledgebaseService,
|
||||
"get_by_id",
|
||||
lambda _id: (True, SimpleNamespace(status=module.StatusEnum.VALID.value, name="KB One")),
|
||||
)
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"dialog_id": "dialog-1",
|
||||
"kb_names": ["legacy"],
|
||||
"name": "new-name",
|
||||
"prompt_config": {"system": "hello", "parameters": []},
|
||||
},
|
||||
)
|
||||
res = _run(handler())
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["name"] == "new-name"
|
||||
assert res["data"]["kb_names"] == ["KB One"]
|
||||
|
||||
def _raise_tenant(_id):
|
||||
raise RuntimeError("set boom")
|
||||
|
||||
monkeypatch.setattr(module.TenantService, "get_by_id", _raise_tenant)
|
||||
_set_request_json(monkeypatch, module, {"name": "demo", "prompt_config": {"system": "hello", "parameters": []}})
|
||||
res = _run(handler())
|
||||
assert "set boom" in res["message"]
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_get_get_kb_names_and_list_dialogs_exception_matrix_unit(monkeypatch):
|
||||
module = _load_dialog_module(monkeypatch)
|
||||
get_handler = inspect.unwrap(module.get)
|
||||
|
||||
monkeypatch.setattr(
|
||||
module.DialogService,
|
||||
"get_by_id",
|
||||
lambda _id: (True, SimpleNamespace(to_dict=lambda: {"id": _id, "kb_ids": ["kb-1", "kb-2"]})),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
module.KnowledgebaseService,
|
||||
"get_by_id",
|
||||
lambda kid: (
|
||||
(True, SimpleNamespace(status=module.StatusEnum.VALID.value, name="KB-1"))
|
||||
if kid == "kb-1"
|
||||
else (False, None)
|
||||
),
|
||||
)
|
||||
_set_request_args(monkeypatch, module, {"dialog_id": "dialog-1"})
|
||||
res = get_handler()
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["kb_ids"] == ["kb-1"]
|
||||
assert res["data"]["kb_names"] == ["KB-1"]
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (False, None))
|
||||
_set_request_args(monkeypatch, module, {"dialog_id": "dialog-missing"})
|
||||
res = get_handler()
|
||||
assert res["message"] == "Dialog not found!"
|
||||
|
||||
def _raise_get(_id):
|
||||
raise RuntimeError("get boom")
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", _raise_get)
|
||||
_set_request_args(monkeypatch, module, {"dialog_id": "dialog-1"})
|
||||
res = get_handler()
|
||||
assert "get boom" in res["message"]
|
||||
|
||||
monkeypatch.setattr(
|
||||
module.KnowledgebaseService,
|
||||
"get_by_id",
|
||||
lambda kid: (
|
||||
(True, SimpleNamespace(status=module.StatusEnum.VALID.value, name=f"KB-{kid}"))
|
||||
if kid.startswith("ok")
|
||||
else (True, SimpleNamespace(status=module.StatusEnum.INVALID.value, name=f"BAD-{kid}"))
|
||||
),
|
||||
)
|
||||
ids, names = module.get_kb_names(["ok-1", "bad-1", "ok-2"])
|
||||
assert ids == ["ok-1", "ok-2"]
|
||||
assert names == ["KB-ok-1", "KB-ok-2"]
|
||||
|
||||
def _raise_list(**_kwargs):
|
||||
raise RuntimeError("list boom")
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "query", _raise_list)
|
||||
res = module.list_dialogs()
|
||||
assert "list boom" in res["message"]
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_list_dialogs_next_owner_desc_and_pagination_matrix_unit(monkeypatch):
|
||||
module = _load_dialog_module(monkeypatch)
|
||||
handler = inspect.unwrap(module.list_dialogs_next)
|
||||
|
||||
calls = []
|
||||
|
||||
def _get_by_tenant_ids(tenants, user_id, page_number, items_per_page, orderby, desc, keywords, parser_id):
|
||||
calls.append(
|
||||
{
|
||||
"tenants": tenants,
|
||||
"user_id": user_id,
|
||||
"page_number": page_number,
|
||||
"items_per_page": items_per_page,
|
||||
"orderby": orderby,
|
||||
"desc": desc,
|
||||
"keywords": keywords,
|
||||
"parser_id": parser_id,
|
||||
}
|
||||
)
|
||||
if tenants:
|
||||
return (
|
||||
[
|
||||
{"id": "dialog-1", "tenant_id": "tenant-a"},
|
||||
{"id": "dialog-2", "tenant_id": "tenant-x"},
|
||||
{"id": "dialog-3", "tenant_id": "tenant-b"},
|
||||
],
|
||||
3,
|
||||
)
|
||||
return ([{"id": "dialog-0", "tenant_id": "tenant-1"}], 1)
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "get_by_tenant_ids", _get_by_tenant_ids)
|
||||
|
||||
_set_request_args(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"keywords": "k",
|
||||
"page": "1",
|
||||
"page_size": "2",
|
||||
"parser_id": "parser-x",
|
||||
"orderby": "create_time",
|
||||
"desc": "false",
|
||||
},
|
||||
)
|
||||
_set_request_json(monkeypatch, module, {"owner_ids": []})
|
||||
res = _run(handler())
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["total"] == 1
|
||||
assert calls[-1]["tenants"] == []
|
||||
assert calls[-1]["desc"] is False
|
||||
|
||||
_set_request_args(monkeypatch, module, {"page": "2", "page_size": "1"})
|
||||
_set_request_json(monkeypatch, module, {"owner_ids": ["tenant-a", "tenant-b"]})
|
||||
res = _run(handler())
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["total"] == 2
|
||||
assert res["data"]["dialogs"] == [{"id": "dialog-3", "tenant_id": "tenant-b"}]
|
||||
assert calls[-1]["page_number"] == 0
|
||||
assert calls[-1]["items_per_page"] == 0
|
||||
assert calls[-1]["desc"] is True
|
||||
|
||||
def _raise_next(*_args, **_kwargs):
|
||||
raise RuntimeError("next boom")
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "get_by_tenant_ids", _raise_next)
|
||||
_set_request_args(monkeypatch, module, {"page": "1", "page_size": "1"})
|
||||
_set_request_json(monkeypatch, module, {"owner_ids": []})
|
||||
res = _run(handler())
|
||||
assert "next boom" in res["message"]
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_rm_permission_and_exception_matrix_unit(monkeypatch):
|
||||
module = _load_dialog_module(monkeypatch)
|
||||
handler = inspect.unwrap(module.rm)
|
||||
|
||||
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-a")])
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
_set_request_json(monkeypatch, module, {"dialog_ids": ["dialog-1"]})
|
||||
res = _run(handler())
|
||||
assert res["code"] == module.RetCode.OPERATING_ERROR
|
||||
assert "Only owner of dialog authorized for this operation." in res["message"]
|
||||
|
||||
def _raise_query(**_kwargs):
|
||||
raise RuntimeError("rm boom")
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "query", _raise_query)
|
||||
_set_request_json(monkeypatch, module, {"dialog_ids": ["dialog-1"]})
|
||||
res = _run(handler())
|
||||
assert "rm boom" in res["message"]
|
||||
177
test/testcases/test_web_api/test_dialog_app/test_get_dialog.py
Normal file
177
test/testcases/test_web_api/test_dialog_app/test_get_dialog.py
Normal file
@@ -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.p2
|
||||
@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
|
||||
210
test/testcases/test_web_api/test_dialog_app/test_list_dialogs.py
Normal file
210
test/testcases/test_web_api/test_dialog_app/test_list_dialogs.py
Normal file
@@ -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.p2
|
||||
@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.p2
|
||||
@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.p2
|
||||
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
|
||||
Reference in New Issue
Block a user