Revert "Refa: Chats /chat API to RESTFul (#13871)" (#13877)

### What problem does this PR solve?

This reverts commit 1a608ac411.

### Type of change

- [x] Other (please describe):
This commit is contained in:
Liu An
2026-04-01 11:05:29 +08:00
committed by GitHub
parent 1a608ac411
commit b1d28b5898
52 changed files with 3584 additions and 2044 deletions

View File

@@ -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}"

View 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])

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View 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

View 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

View File

@@ -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