mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-19 22:21:04 +08:00
Refa: Chat conversations /convsersation API to RESTFul (#13893)
### What problem does this PR solve? Chat conversations /convsersation API to RESTFul. ### Type of change - [x] Refactoring
This commit is contained in:
@@ -678,7 +678,9 @@ def mm_step_12_composer_and_single_send(ctx: FlowContext, step, snap):
|
||||
def _on_completion_request(req):
|
||||
if (
|
||||
req.method.upper() in MM_REQUEST_METHOD_WHITELIST
|
||||
and "/conversation/completion" in req.url
|
||||
and "/api/v1/chats/" in req.url
|
||||
and "/sessions/" in req.url
|
||||
and req.url.rstrip("/").endswith("/completions")
|
||||
):
|
||||
completion_payloads.append(_mm_payload_from_request(req))
|
||||
|
||||
@@ -747,7 +749,7 @@ def mm_step_12_composer_and_single_send(ctx: FlowContext, step, snap):
|
||||
page.remove_listener("request", _on_completion_request)
|
||||
attach_path.unlink(missing_ok=True)
|
||||
|
||||
assert completion_payloads, "no /conversation/completion request was captured"
|
||||
assert completion_payloads, "no chat session completion request was captured"
|
||||
payloads_with_messages = [p for p in completion_payloads if p.get("messages")]
|
||||
assert payloads_with_messages, "completion requests did not include messages"
|
||||
|
||||
|
||||
@@ -58,6 +58,28 @@ class _DummyArgs(dict):
|
||||
return [value]
|
||||
|
||||
|
||||
class _StubHeaders:
|
||||
def __init__(self):
|
||||
self._items = []
|
||||
|
||||
def add_header(self, key, value):
|
||||
self._items.append((key, value))
|
||||
|
||||
def get(self, key, default=None):
|
||||
for existing_key, value in reversed(self._items):
|
||||
if existing_key == key:
|
||||
return value
|
||||
return default
|
||||
|
||||
|
||||
class _StubResponse:
|
||||
def __init__(self, body=None, mimetype=None, content_type=None):
|
||||
self.body = body
|
||||
self.mimetype = mimetype
|
||||
self.content_type = content_type
|
||||
self.headers = _StubHeaders()
|
||||
|
||||
|
||||
def _passthrough_login_required(func):
|
||||
@wraps(func)
|
||||
async def _wrapper(*args, **kwargs):
|
||||
@@ -125,6 +147,7 @@ def _load_chat_module(monkeypatch):
|
||||
|
||||
quart_mod = ModuleType("quart")
|
||||
quart_mod.request = SimpleNamespace(args=_DummyArgs())
|
||||
quart_mod.Response = _StubResponse
|
||||
monkeypatch.setitem(sys.modules, "quart", quart_mod)
|
||||
|
||||
api_pkg = ModuleType("api")
|
||||
@@ -144,6 +167,11 @@ def _load_chat_module(monkeypatch):
|
||||
|
||||
common_constants_mod = ModuleType("common.constants")
|
||||
|
||||
class _StubLLMType(str, Enum):
|
||||
CHAT = "chat"
|
||||
IMAGE2TEXT = "image2text"
|
||||
RERANK = "rerank"
|
||||
|
||||
class _StubRetCode(int, Enum):
|
||||
SUCCESS = 0
|
||||
DATA_ERROR = 102
|
||||
@@ -153,6 +181,7 @@ def _load_chat_module(monkeypatch):
|
||||
VALID = "1"
|
||||
INVALID = "0"
|
||||
|
||||
common_constants_mod.LLMType = _StubLLMType
|
||||
common_constants_mod.RetCode = _StubRetCode
|
||||
common_constants_mod.StatusEnum = _StubStatusEnum
|
||||
monkeypatch.setitem(sys.modules, "common.constants", common_constants_mod)
|
||||
@@ -213,8 +242,42 @@ def _load_chat_module(monkeypatch):
|
||||
return [], 0
|
||||
|
||||
dialog_service_mod.DialogService = _StubDialogService
|
||||
dialog_service_mod.async_ask = lambda *_args, **_kwargs: None
|
||||
dialog_service_mod.async_chat = lambda *_args, **_kwargs: None
|
||||
dialog_service_mod.gen_mindmap = lambda *_args, **_kwargs: None
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.dialog_service", dialog_service_mod)
|
||||
|
||||
conversation_service_mod = ModuleType("api.db.services.conversation_service")
|
||||
|
||||
class _StubConversationService:
|
||||
@staticmethod
|
||||
def query(**_kwargs):
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_list(*_args, **_kwargs):
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(_session_id):
|
||||
return False, None
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(_session_id, _payload):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(_session_id):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def save(**_kwargs):
|
||||
return True
|
||||
|
||||
conversation_service_mod.ConversationService = _StubConversationService
|
||||
conversation_service_mod.structure_answer = lambda *_args, **_kwargs: {}
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.conversation_service", conversation_service_mod)
|
||||
|
||||
kb_service_mod = ModuleType("api.db.services.knowledgebase_service")
|
||||
|
||||
class _StubKnowledgebaseService:
|
||||
@@ -253,6 +316,24 @@ def _load_chat_module(monkeypatch):
|
||||
tenant_llm_service_mod.TenantLLMService = _StubTenantLLMService
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.tenant_llm_service", tenant_llm_service_mod)
|
||||
|
||||
llm_service_mod = ModuleType("api.db.services.llm_service")
|
||||
|
||||
class _StubLLMBundle:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
llm_service_mod.LLMBundle = _StubLLMBundle
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.llm_service", llm_service_mod)
|
||||
|
||||
search_service_mod = ModuleType("api.db.services.search_service")
|
||||
search_service_mod.SearchService = SimpleNamespace()
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.search_service", search_service_mod)
|
||||
|
||||
tenant_model_service_mod = ModuleType("api.db.joint_services.tenant_model_service")
|
||||
tenant_model_service_mod.get_model_config_by_type_and_name = lambda *_args, **_kwargs: {}
|
||||
tenant_model_service_mod.get_tenant_default_model_by_type = lambda *_args, **_kwargs: {}
|
||||
monkeypatch.setitem(sys.modules, "api.db.joint_services.tenant_model_service", tenant_model_service_mod)
|
||||
|
||||
user_service_mod = ModuleType("api.db.services.user_service")
|
||||
|
||||
class _StubTenantService:
|
||||
@@ -283,12 +364,29 @@ def _load_chat_module(monkeypatch):
|
||||
api_utils_mod.get_json_result = lambda data=None, message="", code=0: {"code": code, "data": data, "message": message}
|
||||
api_utils_mod.get_request_json = lambda: _AwaitableValue({})
|
||||
api_utils_mod.server_error_response = lambda ex: {"code": 500, "data": None, "message": str(ex)}
|
||||
api_utils_mod.validate_request = lambda *_args, **_kwargs: (lambda func: func)
|
||||
monkeypatch.setitem(sys.modules, "api.utils.api_utils", api_utils_mod)
|
||||
|
||||
tenant_utils_mod = ModuleType("api.utils.tenant_utils")
|
||||
tenant_utils_mod.ensure_tenant_model_id_for_params = lambda _tenant_id, req: req
|
||||
monkeypatch.setitem(sys.modules, "api.utils.tenant_utils", tenant_utils_mod)
|
||||
|
||||
rag_pkg = ModuleType("rag")
|
||||
rag_pkg.__path__ = [str(repo_root / "rag")]
|
||||
monkeypatch.setitem(sys.modules, "rag", rag_pkg)
|
||||
|
||||
rag_prompts_pkg = ModuleType("rag.prompts")
|
||||
rag_prompts_pkg.__path__ = [str(repo_root / "rag" / "prompts")]
|
||||
monkeypatch.setitem(sys.modules, "rag.prompts", rag_prompts_pkg)
|
||||
|
||||
rag_prompts_generator_mod = ModuleType("rag.prompts.generator")
|
||||
rag_prompts_generator_mod.chunks_format = lambda reference: reference.get("chunks", []) if isinstance(reference, dict) else []
|
||||
monkeypatch.setitem(sys.modules, "rag.prompts.generator", rag_prompts_generator_mod)
|
||||
|
||||
rag_prompts_template_mod = ModuleType("rag.prompts.template")
|
||||
rag_prompts_template_mod.load_prompt = lambda *_args, **_kwargs: ""
|
||||
monkeypatch.setitem(sys.modules, "rag.prompts.template", rag_prompts_template_mod)
|
||||
|
||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
module.manager = _DummyManager()
|
||||
@@ -741,24 +839,148 @@ def test_list_chats_keeps_zero_pagination_semantics(monkeypatch):
|
||||
assert calls[-1] == (0, 2)
|
||||
assert len(res["data"]["chats"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_session_create_and_update_guard_matrix_unit(monkeypatch):
|
||||
module = _load_chat_module(monkeypatch)
|
||||
|
||||
_set_request_json(monkeypatch, module, {"name": "session"})
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
res = _run(module.create_session.__wrapped__("chat-1"))
|
||||
assert res["message"] == "No authorization."
|
||||
|
||||
dia = SimpleNamespace(prompt_config={"prologue": "hello"})
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [dia])
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (True, dia))
|
||||
monkeypatch.setattr(module.ConversationService, "save", lambda **_kwargs: None)
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (False, None))
|
||||
res = _run(module.create_session.__wrapped__("chat-1"))
|
||||
assert "Fail to create a session" in res["message"]
|
||||
|
||||
_set_request_json(monkeypatch, module, {})
|
||||
monkeypatch.setattr(module.ConversationService, "query", lambda **_kwargs: [])
|
||||
res = _run(module.update_session.__wrapped__("chat-1", "session-1"))
|
||||
assert res["message"] == "Session not found!"
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "query", lambda **_kwargs: [SimpleNamespace(id="session-1")])
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
res = _run(module.update_session.__wrapped__("chat-1", "session-1"))
|
||||
assert res["message"] == "No authorization."
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")])
|
||||
_set_request_json(monkeypatch, module, {"message": []})
|
||||
res = _run(module.update_session.__wrapped__("chat-1", "session-1"))
|
||||
assert "`messages` cannot be changed." in res["message"]
|
||||
|
||||
_set_request_json(monkeypatch, module, {"reference": []})
|
||||
res = _run(module.update_session.__wrapped__("chat-1", "session-1"))
|
||||
assert "`reference` cannot be changed." in res["message"]
|
||||
|
||||
_set_request_json(monkeypatch, module, {"name": ""})
|
||||
res = _run(module.update_session.__wrapped__("chat-1", "session-1"))
|
||||
assert "`name` can not be empty." in res["message"]
|
||||
|
||||
_set_request_json(monkeypatch, module, {"name": "renamed"})
|
||||
monkeypatch.setattr(module.ConversationService, "update_by_id", lambda *_args, **_kwargs: False)
|
||||
res = _run(module.update_session.__wrapped__("chat-1", "session-1"))
|
||||
assert res["message"] == "Session not found!"
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_session_list_projection_unit(monkeypatch):
|
||||
module = _load_chat_module(monkeypatch)
|
||||
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"request",
|
||||
SimpleNamespace(
|
||||
args=SimpleNamespace(
|
||||
get=lambda key, default=None: {
|
||||
"keywords": "",
|
||||
"page_size": 2,
|
||||
"page": 1,
|
||||
"page_size": 30,
|
||||
"orderby": "create_time",
|
||||
"desc": "true",
|
||||
}.get(key, default),
|
||||
getlist=lambda _key: [],
|
||||
"id": None,
|
||||
"name": None,
|
||||
"user_id": None,
|
||||
}.get(key, default)
|
||||
)
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")])
|
||||
monkeypatch.setattr(
|
||||
module.ConversationService,
|
||||
"get_list",
|
||||
lambda *_args, **_kwargs: [
|
||||
{
|
||||
"id": "session-1",
|
||||
"dialog_id": "chat-1",
|
||||
"message": [{"role": "assistant", "content": "hello"}],
|
||||
"reference": [],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
res = module.list_chats.__wrapped__()
|
||||
res = module.list_sessions.__wrapped__("chat-1")
|
||||
assert res["data"][0]["chat_id"] == "chat-1"
|
||||
assert res["data"][0]["messages"][0]["content"] == "hello"
|
||||
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"request",
|
||||
SimpleNamespace(
|
||||
args=SimpleNamespace(
|
||||
get=lambda key, default=None: {
|
||||
"page": 1,
|
||||
"page_size": 0,
|
||||
"orderby": "create_time",
|
||||
"desc": "true",
|
||||
"id": None,
|
||||
"name": None,
|
||||
"user_id": None,
|
||||
}.get(key, default)
|
||||
)
|
||||
),
|
||||
)
|
||||
res = module.list_sessions.__wrapped__("chat-1")
|
||||
assert res["data"] == []
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_session_delete_routes_partial_duplicate_unit(monkeypatch):
|
||||
module = _load_chat_module(monkeypatch)
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")])
|
||||
_set_request_json(monkeypatch, module, {})
|
||||
res = _run(module.delete_sessions.__wrapped__("chat-1"))
|
||||
assert res["code"] == 0
|
||||
assert calls[-1] == (0, 2)
|
||||
assert len(res["data"]["chats"]) == 1
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "delete_by_id", lambda *_args, **_kwargs: True)
|
||||
|
||||
def _conversation_query(**kwargs):
|
||||
if "dialog_id" in kwargs and "id" not in kwargs:
|
||||
return [SimpleNamespace(id="seed")]
|
||||
if kwargs.get("id") == "ok":
|
||||
return [SimpleNamespace(id="ok")]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "query", _conversation_query)
|
||||
|
||||
_set_request_json(monkeypatch, module, {"ids": ["ok", "bad"]})
|
||||
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, []))
|
||||
res = _run(module.delete_sessions.__wrapped__("chat-1"))
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["success_count"] == 1
|
||||
assert res["data"]["errors"] == ["The chat doesn't own the session bad"]
|
||||
|
||||
_set_request_json(monkeypatch, module, {"ids": ["bad"]})
|
||||
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, []))
|
||||
res = _run(module.delete_sessions.__wrapped__("chat-1"))
|
||||
assert res["message"] == "The chat doesn't own the session bad"
|
||||
|
||||
_set_request_json(monkeypatch, module, {"ids": ["ok", "ok"]})
|
||||
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (["ok"], ["Duplicate session ids: ok"]))
|
||||
res = _run(module.delete_sessions.__wrapped__("chat-1"))
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["success_count"] == 1
|
||||
assert res["data"]["errors"] == ["Duplicate session ids: ok"]
|
||||
|
||||
@@ -26,12 +26,8 @@ class TestAuthorization:
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_auth, expected_code, expected_message",
|
||||
[
|
||||
(None, 0, "`Authorization` can't be empty"),
|
||||
(
|
||||
RAGFlowHttpApiAuth(INVALID_API_TOKEN),
|
||||
109,
|
||||
"Authentication error: API key is invalid!",
|
||||
),
|
||||
(None, 401, "<Unauthorized '401: Unauthorized'>"),
|
||||
(RAGFlowHttpApiAuth(INVALID_API_TOKEN), 401, "<Unauthorized '401: Unauthorized'>"),
|
||||
],
|
||||
)
|
||||
def test_invalid_auth(self, invalid_auth, expected_code, expected_message):
|
||||
@@ -74,7 +70,7 @@ class TestSessionWithChatAssistantCreate:
|
||||
"chat_assistant_id, expected_code, expected_message",
|
||||
[
|
||||
("", 100, "<MethodNotAllowed '405: Method Not Allowed'>"),
|
||||
("invalid_chat_assistant_id", 102, "You do not own the assistant."),
|
||||
("invalid_chat_assistant_id", 109, "No authorization."),
|
||||
],
|
||||
)
|
||||
def test_invalid_chat_assistant_id(self, HttpApiAuth, chat_assistant_id, expected_code, expected_message):
|
||||
@@ -115,5 +111,5 @@ class TestSessionWithChatAssistantCreate:
|
||||
res = delete_chat_assistants(HttpApiAuth, {"ids": [chat_assistant_ids[0]]})
|
||||
assert res["code"] == 0
|
||||
res = create_session_with_chat_assistant(HttpApiAuth, chat_assistant_ids[0], {"name": "valid_name"})
|
||||
assert res["code"] == 102
|
||||
assert res["message"] == "You do not own the assistant."
|
||||
assert res["code"] == 109
|
||||
assert res["message"] == "No authorization."
|
||||
|
||||
@@ -26,12 +26,8 @@ class TestAuthorization:
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_auth, expected_code, expected_message",
|
||||
[
|
||||
(None, 0, "`Authorization` can't be empty"),
|
||||
(
|
||||
RAGFlowHttpApiAuth(INVALID_API_TOKEN),
|
||||
109,
|
||||
"Authentication error: API key is invalid!",
|
||||
),
|
||||
(None, 401, "<Unauthorized '401: Unauthorized'>"),
|
||||
(RAGFlowHttpApiAuth(INVALID_API_TOKEN), 401, "<Unauthorized '401: Unauthorized'>"),
|
||||
],
|
||||
)
|
||||
def test_invalid_auth(self, invalid_auth, expected_code, expected_message):
|
||||
@@ -48,8 +44,8 @@ class TestSessionWithChatAssistantDelete:
|
||||
("", 100, "<MethodNotAllowed '405: Method Not Allowed'>"),
|
||||
(
|
||||
"invalid_chat_assistant_id",
|
||||
102,
|
||||
"You don't own the chat",
|
||||
109,
|
||||
"No authorization.",
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -146,6 +142,7 @@ class TestSessionWithChatAssistantDelete:
|
||||
pytest.param("not json", 100, """AttributeError("\'str\' object has no attribute \'get\'")""", 5, marks=pytest.mark.skip),
|
||||
pytest.param(lambda r: {"ids": r[:1]}, 0, "", 4, marks=pytest.mark.p3),
|
||||
pytest.param(lambda r: {"ids": r}, 0, "", 0, marks=pytest.mark.p1),
|
||||
pytest.param({"delete_all": True}, 0, "", 0, marks=pytest.mark.p1),
|
||||
pytest.param({"ids": []}, 0, "", 5, marks=pytest.mark.p3),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -27,12 +27,8 @@ class TestAuthorization:
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_auth, expected_code, expected_message",
|
||||
[
|
||||
(None, 0, "`Authorization` can't be empty"),
|
||||
(
|
||||
RAGFlowHttpApiAuth(INVALID_API_TOKEN),
|
||||
109,
|
||||
"Authentication error: API key is invalid!",
|
||||
),
|
||||
(None, 401, "<Unauthorized '401: Unauthorized'>"),
|
||||
(RAGFlowHttpApiAuth(INVALID_API_TOKEN), 401, "<Unauthorized '401: Unauthorized'>"),
|
||||
],
|
||||
)
|
||||
def test_invalid_auth(self, invalid_auth, expected_code, expected_message):
|
||||
@@ -246,5 +242,5 @@ class TestSessionsWithChatAssistantList:
|
||||
assert res["code"] == 0
|
||||
|
||||
res = list_session_with_chat_assistants(HttpApiAuth, chat_assistant_id)
|
||||
assert res["code"] == 102
|
||||
assert "You don't own the assistant" in res["message"]
|
||||
assert res["code"] == 109
|
||||
assert res["message"] == "No authorization."
|
||||
|
||||
@@ -530,18 +530,7 @@ def _load_session_module(monkeypatch):
|
||||
def test_create_and_update_guard_matrix(monkeypatch):
|
||||
module = _load_session_module(monkeypatch)
|
||||
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"name": "session"}))
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
res = _run(inspect.unwrap(module.create)("tenant-1", "chat-1"))
|
||||
assert res["message"] == "You do not own the assistant."
|
||||
|
||||
dia = SimpleNamespace(prompt_config={"prologue": "hello"})
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [dia])
|
||||
monkeypatch.setattr(module.ConversationService, "save", lambda **_kwargs: None)
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (False, None))
|
||||
res = _run(inspect.unwrap(module.create)("tenant-1", "chat-1"))
|
||||
assert "Fail to create a session" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({}))
|
||||
monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args()))
|
||||
monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [SimpleNamespace(id="agent-1")])
|
||||
|
||||
@@ -556,34 +545,6 @@ def test_create_and_update_guard_matrix(monkeypatch):
|
||||
res = _run(inspect.unwrap(module.create_agent_session)("tenant-1", "agent-1"))
|
||||
assert res["message"] == "You cannot access the agent."
|
||||
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({}))
|
||||
monkeypatch.setattr(module.ConversationService, "query", lambda **_kwargs: [])
|
||||
res = _run(inspect.unwrap(module.update)("tenant-1", "chat-1", "session-1"))
|
||||
assert res["message"] == "Session does not exist"
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "query", lambda **_kwargs: [SimpleNamespace(id="session-1")])
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
res = _run(inspect.unwrap(module.update)("tenant-1", "chat-1", "session-1"))
|
||||
assert res["message"] == "You do not own the session"
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")])
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"message": []}))
|
||||
res = _run(inspect.unwrap(module.update)("tenant-1", "chat-1", "session-1"))
|
||||
assert "`message` can not be change" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"reference": []}))
|
||||
res = _run(inspect.unwrap(module.update)("tenant-1", "chat-1", "session-1"))
|
||||
assert "`reference` can not be change" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"name": ""}))
|
||||
res = _run(inspect.unwrap(module.update)("tenant-1", "chat-1", "session-1"))
|
||||
assert "`name` can not be empty" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"name": "renamed"}))
|
||||
monkeypatch.setattr(module.ConversationService, "update_by_id", lambda *_args, **_kwargs: False)
|
||||
res = _run(inspect.unwrap(module.update)("tenant-1", "chat-1", "session-1"))
|
||||
assert res["message"] == "Session updates error"
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_completion_metadata_and_stream_paths(monkeypatch):
|
||||
@@ -929,44 +890,6 @@ def test_agent_completions_stream_and_nonstream_unit(monkeypatch):
|
||||
assert res["data"].startswith("**ERROR**")
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_list_session_projection_unit(monkeypatch):
|
||||
module = _load_session_module(monkeypatch)
|
||||
|
||||
monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args({})))
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")])
|
||||
|
||||
convs = [
|
||||
{
|
||||
"id": "session-1",
|
||||
"dialog_id": "chat-1",
|
||||
"message": [{"role": "assistant", "content": "hello", "prompt": "internal"}],
|
||||
"reference": [
|
||||
{
|
||||
"chunks": [
|
||||
{
|
||||
"chunk_id": "chunk-1",
|
||||
"content_with_weight": "weighted",
|
||||
"doc_id": "doc-1",
|
||||
"docnm_kwd": "doc-name",
|
||||
"kb_id": "kb-1",
|
||||
"image_id": "img-1",
|
||||
"positions": [1, 2],
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
monkeypatch.setattr(module.ConversationService, "get_list", lambda *_args, **_kwargs: convs)
|
||||
|
||||
res = _run(inspect.unwrap(module.list_session)("tenant-1", "chat-1"))
|
||||
assert res["data"][0]["chat_id"] == "chat-1"
|
||||
assert "reference" not in res["data"][0]
|
||||
assert "prompt" not in res["data"][0]["messages"][0]
|
||||
assert res["data"][0]["messages"][0]["reference"][0]["positions"] == [1, 2]
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_list_agent_session_projection_unit(monkeypatch):
|
||||
module = _load_session_module(monkeypatch)
|
||||
@@ -1020,41 +943,6 @@ def test_list_agent_session_projection_unit(monkeypatch):
|
||||
def test_delete_routes_partial_duplicate_unit(monkeypatch):
|
||||
module = _load_session_module(monkeypatch)
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")])
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({}))
|
||||
res = _run(inspect.unwrap(module.delete)("tenant-1", "chat-1"))
|
||||
assert res["code"] == 0
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "delete_by_id", lambda *_args, **_kwargs: True)
|
||||
|
||||
def _conversation_query(**kwargs):
|
||||
if "id" not in kwargs:
|
||||
return [SimpleNamespace(id="seed")]
|
||||
if kwargs["id"] == "ok":
|
||||
return [SimpleNamespace(id="ok")]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "query", _conversation_query)
|
||||
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"ids": ["ok", "bad"]}))
|
||||
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, []))
|
||||
res = _run(inspect.unwrap(module.delete)("tenant-1", "chat-1"))
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["success_count"] == 1
|
||||
assert res["data"]["errors"] == ["The chat doesn't own the session bad"]
|
||||
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"ids": ["bad"]}))
|
||||
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, []))
|
||||
res = _run(inspect.unwrap(module.delete)("tenant-1", "chat-1"))
|
||||
assert res["message"] == "The chat doesn't own the session bad"
|
||||
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"ids": ["ok", "ok"]}))
|
||||
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (["ok"], ["Duplicate session ids: ok"]))
|
||||
res = _run(inspect.unwrap(module.delete)("tenant-1", "chat-1"))
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["success_count"] == 1
|
||||
assert res["data"]["errors"] == ["Duplicate session ids: ok"]
|
||||
|
||||
monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [SimpleNamespace(id="agent-1")])
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({}))
|
||||
res = _run(inspect.unwrap(module.delete_agent_session)("tenant-1", "agent-1"))
|
||||
|
||||
@@ -27,12 +27,8 @@ class TestAuthorization:
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_auth, expected_code, expected_message",
|
||||
[
|
||||
(None, 0, "`Authorization` can't be empty"),
|
||||
(
|
||||
RAGFlowHttpApiAuth(INVALID_API_TOKEN),
|
||||
109,
|
||||
"Authentication error: API key is invalid!",
|
||||
),
|
||||
(None, 401, "<Unauthorized '401: Unauthorized'>"),
|
||||
(RAGFlowHttpApiAuth(INVALID_API_TOKEN), 401, "<Unauthorized '401: Unauthorized'>"),
|
||||
],
|
||||
)
|
||||
def test_invalid_auth(self, invalid_auth, expected_code, expected_message):
|
||||
@@ -72,7 +68,7 @@ class TestSessionWithChatAssistantUpdate:
|
||||
@pytest.mark.parametrize(
|
||||
"chat_assistant_id, expected_code, expected_message",
|
||||
[
|
||||
(INVALID_ID_32, 102, "Session does not exist"),
|
||||
(INVALID_ID_32, 109, "No authorization."),
|
||||
],
|
||||
)
|
||||
def test_invalid_chat_assistant_id(self, HttpApiAuth, add_sessions_with_chat_assistant_func, chat_assistant_id, expected_code, expected_message):
|
||||
@@ -86,7 +82,7 @@ class TestSessionWithChatAssistantUpdate:
|
||||
"session_id, expected_code, expected_message",
|
||||
[
|
||||
("", 100, "<MethodNotAllowed '405: Method Not Allowed'>"),
|
||||
("invalid_session_id", 102, "Session does not exist"),
|
||||
("invalid_session_id", 102, "Session not found!"),
|
||||
],
|
||||
)
|
||||
def test_invalid_session_id(self, HttpApiAuth, add_sessions_with_chat_assistant_func, session_id, expected_code, expected_message):
|
||||
@@ -145,5 +141,5 @@ class TestSessionWithChatAssistantUpdate:
|
||||
chat_assistant_id, session_ids = add_sessions_with_chat_assistant_func
|
||||
delete_chat_assistants(HttpApiAuth, {"ids": [chat_assistant_id]})
|
||||
res = update_session_with_chat_assistant(HttpApiAuth, chat_assistant_id, session_ids[0], {"name": "valid_name"})
|
||||
assert res["code"] == 102
|
||||
assert res["message"] == "You do not own the session"
|
||||
assert res["code"] == 109
|
||||
assert res["message"] == "No authorization."
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#
|
||||
# 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 pytest
|
||||
from ragflow_sdk import RAGFlow
|
||||
from ragflow_sdk.modules.chat import Chat
|
||||
from ragflow_sdk.modules.session import Session
|
||||
|
||||
|
||||
class _DummyResponse:
|
||||
def __init__(self, payload):
|
||||
self._payload = payload
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def auth():
|
||||
return "unit-auth"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def set_tenant_info():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_create_session_raises_server_error_message(monkeypatch):
|
||||
client = RAGFlow("token", "http://localhost:9380")
|
||||
chat = Chat(client, {"id": "chat-1"})
|
||||
|
||||
monkeypatch.setattr(
|
||||
chat,
|
||||
"post",
|
||||
lambda *_args, **_kwargs: _DummyResponse({"code": 102, "message": "`name` can not be empty."}),
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
chat.create_session(name="")
|
||||
assert "`name` can not be empty." in str(exception_info.value), str(exception_info.value)
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_list_sessions_forwards_restful_query_params(monkeypatch):
|
||||
client = RAGFlow("token", "http://localhost:9380")
|
||||
chat = Chat(client, {"id": "chat-1"})
|
||||
calls = []
|
||||
|
||||
def _ok_get(path, params=None):
|
||||
calls.append((path, params))
|
||||
return _DummyResponse(
|
||||
{
|
||||
"code": 0,
|
||||
"data": [
|
||||
{"id": "session-1", "chat_id": "chat-1", "name": "one"},
|
||||
{"id": "session-2", "chat_id": "chat-1", "name": "two"},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(chat, "get", _ok_get)
|
||||
|
||||
sessions = chat.list_sessions(page=2, page_size=2, orderby="create_time", desc=False, id="session-1", name="one", user_id="user-1")
|
||||
assert len(sessions) == 2, str(sessions)
|
||||
assert all(isinstance(item, Session) for item in sessions), str(sessions)
|
||||
assert calls[-1][0] == "/chats/chat-1/sessions"
|
||||
assert calls[-1][1]["page_size"] == 2
|
||||
assert calls[-1][1]["name"] == "one"
|
||||
assert calls[-1][1]["user_id"] == "user-1"
|
||||
|
||||
all_sessions = chat.list_sessions(page_size=0)
|
||||
assert len(all_sessions) == 2, str(all_sessions)
|
||||
assert calls[-1][1]["page_size"] == 0
|
||||
@@ -1,801 +0,0 @@
|
||||
#
|
||||
# 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 sys
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from anyio import Path as AsyncPath
|
||||
|
||||
|
||||
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 _DummyRequest:
|
||||
def __init__(self, *, args=None, headers=None, form=None, files=None):
|
||||
self.args = args or {}
|
||||
self.headers = headers or {}
|
||||
self.form = _AwaitableValue(form or {})
|
||||
self.files = _AwaitableValue(files or {})
|
||||
self.method = "POST"
|
||||
self.content_length = 0
|
||||
|
||||
|
||||
class _DummyConversation:
|
||||
def __init__(self, *, conv_id="conv-1", dialog_id="dialog-1", message=None, reference=None):
|
||||
self.id = conv_id
|
||||
self.dialog_id = dialog_id
|
||||
self.message = message if message is not None else []
|
||||
self.reference = reference if reference is not None else []
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"dialog_id": self.dialog_id,
|
||||
"message": deepcopy(self.message),
|
||||
"reference": deepcopy(self.reference),
|
||||
}
|
||||
|
||||
|
||||
class _DummyDialog:
|
||||
def __init__(self, *, dialog_id="dialog-1", tenant_id="tenant-1", icon="avatar.png"):
|
||||
self.id = dialog_id
|
||||
self.tenant_id = tenant_id
|
||||
self.icon = icon
|
||||
self.prompt_config = {"prologue": "hello"}
|
||||
self.llm_id = ""
|
||||
self.llm_setting = {}
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"icon": self.icon,
|
||||
"tenant_id": self.tenant_id,
|
||||
"prompt_config": deepcopy(self.prompt_config),
|
||||
}
|
||||
|
||||
|
||||
class _DummyUploadedFile:
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self.saved_path = None
|
||||
|
||||
async def save(self, path):
|
||||
self.saved_path = path
|
||||
await AsyncPath(path).write_bytes(b"audio-bytes")
|
||||
|
||||
|
||||
def _run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def _load_conversation_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)
|
||||
|
||||
deepdoc_pkg = ModuleType("deepdoc")
|
||||
deepdoc_parser_pkg = ModuleType("deepdoc.parser")
|
||||
deepdoc_parser_pkg.__path__ = []
|
||||
|
||||
class _StubPdfParser:
|
||||
pass
|
||||
|
||||
class _StubExcelParser:
|
||||
pass
|
||||
|
||||
class _StubDocxParser:
|
||||
pass
|
||||
|
||||
deepdoc_parser_pkg.PdfParser = _StubPdfParser
|
||||
deepdoc_parser_pkg.ExcelParser = _StubExcelParser
|
||||
deepdoc_parser_pkg.DocxParser = _StubDocxParser
|
||||
deepdoc_pkg.parser = deepdoc_parser_pkg
|
||||
monkeypatch.setitem(sys.modules, "deepdoc", deepdoc_pkg)
|
||||
monkeypatch.setitem(sys.modules, "deepdoc.parser", deepdoc_parser_pkg)
|
||||
|
||||
deepdoc_excel_module = ModuleType("deepdoc.parser.excel_parser")
|
||||
deepdoc_excel_module.RAGFlowExcelParser = _StubExcelParser
|
||||
monkeypatch.setitem(sys.modules, "deepdoc.parser.excel_parser", deepdoc_excel_module)
|
||||
|
||||
deepdoc_parser_utils = ModuleType("deepdoc.parser.utils")
|
||||
deepdoc_parser_utils.get_text = lambda *_args, **_kwargs: ""
|
||||
monkeypatch.setitem(sys.modules, "deepdoc.parser.utils", deepdoc_parser_utils)
|
||||
monkeypatch.setitem(sys.modules, "xgboost", ModuleType("xgboost"))
|
||||
|
||||
apps_mod = ModuleType("api.apps")
|
||||
apps_mod.current_user = SimpleNamespace(id="user-1")
|
||||
apps_mod.login_required = lambda func: func
|
||||
monkeypatch.setitem(sys.modules, "api.apps", apps_mod)
|
||||
|
||||
# Create user_service module with TenantService stub if not already exists
|
||||
if "api.db.services.user_service" not in sys.modules:
|
||||
user_service_mod = ModuleType("api.db.services.user_service")
|
||||
user_service_mod.UserService = SimpleNamespace() # Dummy UserService class
|
||||
user_service_mod.TenantService = SimpleNamespace(
|
||||
get_info_by=lambda _uid: [],
|
||||
get_by_id=lambda _uid: (False, None)
|
||||
)
|
||||
user_service_mod.UserTenantService = SimpleNamespace(
|
||||
query=lambda **_kwargs: []
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.user_service", user_service_mod)
|
||||
|
||||
module_name = "test_conversation_routes_unit_module"
|
||||
module_path = repo_root / "api" / "apps" / "conversation_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
|
||||
|
||||
|
||||
def _set_request_json(monkeypatch, module, payload):
|
||||
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue(deepcopy(payload)))
|
||||
|
||||
|
||||
async def _read_sse_text(response):
|
||||
chunks = []
|
||||
async for chunk in response.response:
|
||||
if isinstance(chunk, bytes):
|
||||
chunks.append(chunk.decode("utf-8"))
|
||||
else:
|
||||
chunks.append(chunk)
|
||||
return "".join(chunks)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def auth():
|
||||
return "unit-auth"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def set_tenant_info():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_set_conversation_update_create_and_errors(monkeypatch):
|
||||
module = _load_conversation_module(monkeypatch)
|
||||
|
||||
long_name = "n" * 300
|
||||
create_payload = {
|
||||
"conversation_id": "conv-new",
|
||||
"dialog_id": "dialog-1",
|
||||
"is_new": True,
|
||||
"name": long_name,
|
||||
}
|
||||
_set_request_json(monkeypatch, module, create_payload)
|
||||
|
||||
saved = {}
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (True, _DummyDialog()))
|
||||
monkeypatch.setattr(module.ConversationService, "save", lambda **kwargs: saved.update(kwargs) or True)
|
||||
res = _run(module.set_conversation())
|
||||
assert res["code"] == 0
|
||||
assert len(res["data"]["name"]) == 255
|
||||
assert saved["user_id"] == "user-1"
|
||||
|
||||
update_payload = {
|
||||
"conversation_id": "conv-1",
|
||||
"dialog_id": "dialog-1",
|
||||
"is_new": False,
|
||||
"name": "rename",
|
||||
}
|
||||
_set_request_json(monkeypatch, module, update_payload)
|
||||
monkeypatch.setattr(module.ConversationService, "update_by_id", lambda *_args, **_kwargs: False)
|
||||
res = _run(module.set_conversation())
|
||||
assert "Conversation not found" in res["message"]
|
||||
|
||||
_set_request_json(monkeypatch, module, update_payload)
|
||||
monkeypatch.setattr(module.ConversationService, "update_by_id", lambda *_args, **_kwargs: True)
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (False, None))
|
||||
res = _run(module.set_conversation())
|
||||
assert "Fail to update" in res["message"]
|
||||
|
||||
_set_request_json(monkeypatch, module, update_payload)
|
||||
monkeypatch.setattr(module.ConversationService, "update_by_id", lambda *_args, **_kwargs: True)
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (True, _DummyConversation(conv_id="conv-1")))
|
||||
res = _run(module.set_conversation())
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["id"] == "conv-1"
|
||||
|
||||
_set_request_json(monkeypatch, module, update_payload)
|
||||
|
||||
def _raise_update(*_args, **_kwargs):
|
||||
raise RuntimeError("update boom")
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "update_by_id", _raise_update)
|
||||
res = _run(module.set_conversation())
|
||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||
assert "update boom" in res["message"]
|
||||
|
||||
missing_dialog_payload = {
|
||||
"conversation_id": "conv-2",
|
||||
"dialog_id": "dialog-missing",
|
||||
"is_new": True,
|
||||
"name": "create",
|
||||
}
|
||||
_set_request_json(monkeypatch, module, missing_dialog_payload)
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (False, None))
|
||||
res = _run(module.set_conversation())
|
||||
assert res["message"] == "Dialog not found"
|
||||
|
||||
_set_request_json(monkeypatch, module, missing_dialog_payload)
|
||||
|
||||
def _raise_dialog(_id):
|
||||
raise RuntimeError("dialog boom")
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", _raise_dialog)
|
||||
res = _run(module.set_conversation())
|
||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||
assert "dialog boom" in res["message"]
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_get_and_getsse_authorization_and_reference_paths(monkeypatch):
|
||||
module = _load_conversation_module(monkeypatch)
|
||||
|
||||
conv = _DummyConversation(reference=[{"doc": "d"}, ["already-formatted"]])
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(args={"conversation_id": "conv-1"}))
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (True, conv))
|
||||
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-1")])
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(icon="bot-avatar")])
|
||||
monkeypatch.setattr(module, "chunks_format", lambda _ref: [{"chunk": "normalized"}])
|
||||
|
||||
res = _run(module.get())
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["avatar"] == "bot-avatar"
|
||||
assert res["data"]["reference"][0]["chunks"] == [{"chunk": "normalized"}]
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (False, None))
|
||||
res = _run(module.get())
|
||||
assert res["message"] == "Conversation not found!"
|
||||
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(args={"conversation_id": "conv-1"}))
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (True, conv))
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
res = _run(module.get())
|
||||
assert res["code"] == module.RetCode.OPERATING_ERROR
|
||||
assert "Only owner of conversation" in res["message"]
|
||||
|
||||
def _raise_get(*_args, **_kwargs):
|
||||
raise RuntimeError("get boom")
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", _raise_get)
|
||||
res = _run(module.get())
|
||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||
assert "get boom" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(headers={"Authorization": "Bearer"}))
|
||||
res = module.getsse("dialog-1")
|
||||
assert "Authorization is not valid" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(headers={"Authorization": "Bearer token-1"}))
|
||||
monkeypatch.setattr(module.APIToken, "query", lambda **_kwargs: [])
|
||||
res = module.getsse("dialog-1")
|
||||
assert "API key is invalid" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module.APIToken, "query", lambda **_kwargs: [SimpleNamespace()])
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (False, None))
|
||||
res = module.getsse("dialog-1")
|
||||
assert res["message"] == "Dialog not found!"
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (True, _DummyDialog()))
|
||||
res = module.getsse("dialog-1")
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["avatar"] == "avatar.png"
|
||||
assert "icon" not in res["data"]
|
||||
|
||||
def _raise_getsse(_id):
|
||||
raise RuntimeError("getsse boom")
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", _raise_getsse)
|
||||
res = module.getsse("dialog-1")
|
||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||
assert "getsse boom" in res["message"]
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_rm_and_list_conversation_guards(monkeypatch):
|
||||
module = _load_conversation_module(monkeypatch)
|
||||
|
||||
_set_request_json(monkeypatch, module, {"conversation_ids": ["conv-1"]})
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (False, None))
|
||||
res = _run(module.rm())
|
||||
assert "Conversation not found" in res["message"]
|
||||
|
||||
conv = _DummyConversation(conv_id="conv-1", dialog_id="dialog-1")
|
||||
_set_request_json(monkeypatch, module, {"conversation_ids": ["conv-1"]})
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (True, conv))
|
||||
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-1")])
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
res = _run(module.rm())
|
||||
assert res["code"] == module.RetCode.OPERATING_ERROR
|
||||
|
||||
deleted = []
|
||||
_set_request_json(monkeypatch, module, {"conversation_ids": ["conv-1"]})
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="dialog-1")])
|
||||
monkeypatch.setattr(module.ConversationService, "delete_by_id", lambda cid: deleted.append(cid) or True)
|
||||
res = _run(module.rm())
|
||||
assert res["code"] == 0
|
||||
assert res["data"] is True
|
||||
assert deleted == ["conv-1"]
|
||||
|
||||
_set_request_json(monkeypatch, module, {"conversation_ids": ["conv-1"]})
|
||||
|
||||
def _raise_rm(*_args, **_kwargs):
|
||||
raise RuntimeError("rm boom")
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", _raise_rm)
|
||||
res = _run(module.rm())
|
||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||
assert "rm boom" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(args={"dialog_id": "dialog-1"}))
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
|
||||
res = _run(module.list_conversation())
|
||||
assert res["code"] == module.RetCode.OPERATING_ERROR
|
||||
assert "Only owner of dialog" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="dialog-1")])
|
||||
monkeypatch.setattr(module.ConversationService, "model", SimpleNamespace(create_time="create_time"))
|
||||
monkeypatch.setattr(module.ConversationService, "query", lambda **_kwargs: [_DummyConversation(conv_id="c1"), _DummyConversation(conv_id="c2")])
|
||||
res = _run(module.list_conversation())
|
||||
assert res["code"] == 0
|
||||
assert [x["id"] for x in res["data"]] == ["c1", "c2"]
|
||||
|
||||
def _raise_list(**_kwargs):
|
||||
raise RuntimeError("list boom")
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "query", _raise_list)
|
||||
res = _run(module.list_conversation())
|
||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||
assert "list boom" in res["message"]
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_completion_stream_and_nonstream_branches(monkeypatch):
|
||||
module = _load_conversation_module(monkeypatch)
|
||||
|
||||
conv = _DummyConversation(conv_id="conv-1", dialog_id="dialog-1", reference=[])
|
||||
dia = _DummyDialog(dialog_id="dialog-1", tenant_id="tenant-1")
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (True, conv))
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (True, dia))
|
||||
monkeypatch.setattr(module, "structure_answer", lambda _conv, ans, message_id, conv_id: {"answer": ans["answer"], "id": message_id, "conversation_id": conv_id, "reference": []})
|
||||
|
||||
updates = []
|
||||
monkeypatch.setattr(module.ConversationService, "update_by_id", lambda conv_id, payload: updates.append((conv_id, payload)) or True)
|
||||
|
||||
stream_payload = {
|
||||
"conversation_id": "conv-1",
|
||||
"messages": [
|
||||
{"role": "system", "content": "ignored"},
|
||||
{"role": "assistant", "content": "ignored-first-assistant"},
|
||||
{"role": "user", "content": "hello", "id": "m-1"},
|
||||
],
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
async def _stream_ok(_dia, sanitized, *_args, **_kwargs):
|
||||
assert [m["role"] for m in sanitized] == ["user"]
|
||||
yield {"answer": "sse-ok"}
|
||||
|
||||
monkeypatch.setattr(module, "async_chat", _stream_ok)
|
||||
_set_request_json(monkeypatch, module, stream_payload)
|
||||
resp = _run(module.completion.__wrapped__())
|
||||
assert resp.headers["Content-Type"].startswith("text/event-stream")
|
||||
sse_text = _run(_read_sse_text(resp))
|
||||
assert "sse-ok" in sse_text
|
||||
assert '"data": true' in sse_text
|
||||
assert updates
|
||||
|
||||
async def _stream_error(_dia, _sanitized, *_args, **_kwargs):
|
||||
raise RuntimeError("stream explode")
|
||||
if False:
|
||||
yield {"answer": "never"}
|
||||
|
||||
monkeypatch.setattr(module, "async_chat", _stream_error)
|
||||
_set_request_json(monkeypatch, module, stream_payload)
|
||||
resp = _run(module.completion.__wrapped__())
|
||||
sse_text = _run(_read_sse_text(resp))
|
||||
assert "**ERROR**: stream explode" in sse_text
|
||||
|
||||
async def _non_stream(_dia, _sanitized, **_kwargs):
|
||||
yield {"answer": "plain-ok"}
|
||||
|
||||
monkeypatch.setattr(module, "async_chat", _non_stream)
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"conversation_id": "conv-1",
|
||||
"messages": [{"role": "user", "content": "plain", "id": "m-2"}],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
res = _run(module.completion.__wrapped__())
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["answer"] == "plain-ok"
|
||||
|
||||
monkeypatch.setattr(module.TenantLLMService, "get_api_key", lambda **_kwargs: False)
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"conversation_id": "conv-1",
|
||||
"messages": [{"role": "user", "content": "embed", "id": "m-3"}],
|
||||
"llm_id": "bad-model",
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
res = _run(module.completion.__wrapped__())
|
||||
assert "Cannot use specified model bad-model" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module.TenantLLMService, "get_api_key", lambda **_kwargs: "api-key")
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"conversation_id": "conv-1",
|
||||
"messages": [{"role": "user", "content": "embed", "id": "m-4"}],
|
||||
"llm_id": "glm-4",
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.2,
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
res = _run(module.completion.__wrapped__())
|
||||
assert res["code"] == 0
|
||||
assert dia.llm_id == "glm-4"
|
||||
assert dia.llm_setting == {"temperature": 0.7, "top_p": 0.2}
|
||||
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"conversation_id": "missing",
|
||||
"messages": [{"role": "user", "content": "x", "id": "m-5"}],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (False, None))
|
||||
res = _run(module.completion.__wrapped__())
|
||||
assert res["message"] == "Conversation not found!"
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (True, conv))
|
||||
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (False, None))
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"conversation_id": "conv-1",
|
||||
"messages": [{"role": "user", "content": "x", "id": "m-6"}],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
res = _run(module.completion.__wrapped__())
|
||||
assert res["message"] == "Dialog not found!"
|
||||
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (_ for _ in ()).throw(RuntimeError("completion boom")))
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{
|
||||
"conversation_id": "conv-1",
|
||||
"messages": [{"role": "user", "content": "x", "id": "m-7"}],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
res = _run(module.completion.__wrapped__())
|
||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||
assert "completion boom" in res["message"]
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_sequence2txt_validation_and_transcription_paths(monkeypatch):
|
||||
module = _load_conversation_module(monkeypatch)
|
||||
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"stream": "false"}, files={}))
|
||||
res = _run(module.sequence2txt())
|
||||
assert "Missing 'file'" in res["message"]
|
||||
|
||||
bad_file = _DummyUploadedFile("audio.txt")
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"stream": "false"}, files={"file": bad_file}))
|
||||
res = _run(module.sequence2txt())
|
||||
assert "Unsupported audio format" in res["message"]
|
||||
|
||||
wav_file = _DummyUploadedFile("audio.wav")
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"stream": "false"}, files={"file": wav_file}))
|
||||
monkeypatch.setattr(module, "get_tenant_default_model_by_type", lambda *_args, **_kwargs: (_ for _ in ()).throw(LookupError("Tenant not found")))
|
||||
res = _run(module.sequence2txt())
|
||||
assert res["message"] == "Tenant not found"
|
||||
|
||||
wav_file = _DummyUploadedFile("audio.wav")
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"stream": "false"}, files={"file": wav_file}))
|
||||
monkeypatch.setattr(module, "get_tenant_default_model_by_type", lambda *_args, **_kwargs: (_ for _ in ()).throw(Exception("No default speech2text model is set.")))
|
||||
res = _run(module.sequence2txt())
|
||||
assert res["message"] == "No default speech2text model is set."
|
||||
|
||||
class _SyncAsr:
|
||||
def transcription(self, _path):
|
||||
return "transcribed text"
|
||||
|
||||
def stream_transcription(self, _path):
|
||||
return []
|
||||
|
||||
wav_file = _DummyUploadedFile("audio.wav")
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"stream": "false"}, files={"file": wav_file}))
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"get_tenant_default_model_by_type",
|
||||
lambda *_args, **_kwargs: {"llm_factory": "test", "llm_name": "asr-model", "model_type": module.LLMType.SPEECH2TEXT.value},
|
||||
)
|
||||
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: _SyncAsr())
|
||||
monkeypatch.setattr(module.os, "remove", lambda _path: (_ for _ in ()).throw(RuntimeError("remove failed")))
|
||||
res = _run(module.sequence2txt())
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["text"] == "transcribed text"
|
||||
|
||||
class _StreamAsr:
|
||||
def transcription(self, _path):
|
||||
return ""
|
||||
|
||||
def stream_transcription(self, _path):
|
||||
yield {"event": "partial", "text": "hello"}
|
||||
|
||||
wav_file = _DummyUploadedFile("audio.wav")
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"stream": "true"}, files={"file": wav_file}))
|
||||
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: _StreamAsr())
|
||||
resp = _run(module.sequence2txt())
|
||||
assert resp.headers["Content-Type"].startswith("text/event-stream")
|
||||
sse_text = _run(_read_sse_text(resp))
|
||||
assert '"event": "partial"' in sse_text
|
||||
|
||||
class _ErrorStreamAsr:
|
||||
def transcription(self, _path):
|
||||
return ""
|
||||
|
||||
def stream_transcription(self, _path):
|
||||
raise RuntimeError("stream asr boom")
|
||||
|
||||
wav_file = _DummyUploadedFile("audio.wav")
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"stream": "true"}, files={"file": wav_file}))
|
||||
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: _ErrorStreamAsr())
|
||||
resp = _run(module.sequence2txt())
|
||||
sse_text = _run(_read_sse_text(resp))
|
||||
assert "stream asr boom" in sse_text
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_tts_request_parse_entry(monkeypatch):
|
||||
module = _load_conversation_module(monkeypatch)
|
||||
_set_request_json(monkeypatch, module, {"text": "A。B"})
|
||||
monkeypatch.setattr(module, "get_tenant_default_model_by_type", lambda *_args, **_kwargs: (_ for _ in ()).throw(LookupError("Tenant not found")))
|
||||
res = _run(module.tts())
|
||||
assert res["message"] == "Tenant not found"
|
||||
|
||||
monkeypatch.setattr(module, "get_tenant_default_model_by_type", lambda *_args, **_kwargs: (_ for _ in ()).throw(Exception("No default tts model is set.")))
|
||||
res = _run(module.tts())
|
||||
assert res["message"] == "No default tts model is set."
|
||||
|
||||
class _TTSOk:
|
||||
def tts(self, txt):
|
||||
if not txt:
|
||||
return []
|
||||
yield f"chunk-{txt}".encode("utf-8")
|
||||
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"get_tenant_default_model_by_type",
|
||||
lambda *_args, **_kwargs: {"llm_factory": "test", "llm_name": "tts-x", "model_type": module.LLMType.TTS.value},
|
||||
)
|
||||
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: _TTSOk())
|
||||
resp = _run(module.tts())
|
||||
assert resp.mimetype == "audio/mpeg"
|
||||
assert resp.headers.get("Cache-Control") == "no-cache"
|
||||
assert resp.headers.get("Connection") == "keep-alive"
|
||||
assert resp.headers.get("X-Accel-Buffering") == "no"
|
||||
stream_text = _run(_read_sse_text(resp))
|
||||
assert "chunk-A" in stream_text
|
||||
assert "chunk-B" in stream_text
|
||||
|
||||
class _TTSErr:
|
||||
def tts(self, _txt):
|
||||
raise RuntimeError("tts boom")
|
||||
|
||||
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: _TTSErr())
|
||||
resp = _run(module.tts())
|
||||
stream_text = _run(_read_sse_text(resp))
|
||||
assert '"code": 500' in stream_text
|
||||
assert "**ERROR**: tts boom" in stream_text
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_delete_msg_and_thumbup_matrix_unit(monkeypatch):
|
||||
module = _load_conversation_module(monkeypatch)
|
||||
|
||||
updates = []
|
||||
monkeypatch.setattr(module.ConversationService, "update_by_id", lambda conv_id, payload: updates.append((conv_id, payload)) or True)
|
||||
|
||||
_set_request_json(monkeypatch, module, {"conversation_id": "missing", "message_id": "pair-1"})
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (False, None))
|
||||
res = _run(module.delete_msg.__wrapped__())
|
||||
assert res["message"] == "Conversation not found!"
|
||||
|
||||
conv = _DummyConversation(
|
||||
conv_id="conv-del",
|
||||
message=[
|
||||
{"id": "other", "role": "user"},
|
||||
{"id": "pair-1", "role": "user"},
|
||||
{"id": "pair-1", "role": "assistant"},
|
||||
],
|
||||
reference=[{"chunks": [{"id": "c1"}]}],
|
||||
)
|
||||
_set_request_json(monkeypatch, module, {"conversation_id": "conv-del", "message_id": "pair-1"})
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (True, conv))
|
||||
res = _run(module.delete_msg.__wrapped__())
|
||||
assert res["code"] == 0
|
||||
assert [m["id"] for m in res["data"]["message"]] == ["other"]
|
||||
assert res["data"]["reference"] == []
|
||||
assert updates[-1][0] == "conv-del"
|
||||
|
||||
_set_request_json(monkeypatch, module, {"conversation_id": "missing", "message_id": "assistant-1", "thumbup": True})
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (False, None))
|
||||
res = _run(module.thumbup.__wrapped__())
|
||||
assert res["message"] == "Conversation not found!"
|
||||
|
||||
conv_up = _DummyConversation(
|
||||
conv_id="conv-up",
|
||||
message=[{"id": "assistant-1", "role": "assistant", "feedback": "old"}],
|
||||
)
|
||||
_set_request_json(monkeypatch, module, {"conversation_id": "conv-up", "message_id": "assistant-1", "thumbup": True})
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (True, conv_up))
|
||||
res = _run(module.thumbup.__wrapped__())
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["message"][0]["thumbup"] is True
|
||||
assert "feedback" not in res["data"]["message"][0]
|
||||
|
||||
conv_down = _DummyConversation(conv_id="conv-down", message=[{"id": "assistant-2", "role": "assistant"}])
|
||||
_set_request_json(
|
||||
monkeypatch,
|
||||
module,
|
||||
{"conversation_id": "conv-down", "message_id": "assistant-2", "thumbup": False, "feedback": "needs sources"},
|
||||
)
|
||||
monkeypatch.setattr(module.ConversationService, "get_by_id", lambda _id: (True, conv_down))
|
||||
res = _run(module.thumbup.__wrapped__())
|
||||
assert res["code"] == 0
|
||||
assert res["data"]["message"][0]["thumbup"] is False
|
||||
assert res["data"]["message"][0]["feedback"] == "needs sources"
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_ask_about_stream_search_config_matrix_unit(monkeypatch):
|
||||
module = _load_conversation_module(monkeypatch)
|
||||
_set_request_json(monkeypatch, module, {"question": "q", "kb_ids": ["kb-1"], "search_id": "search-1"})
|
||||
monkeypatch.setattr(module.SearchService, "get_detail", lambda _sid: {"search_config": {"mode": "test"}})
|
||||
|
||||
captured = {}
|
||||
|
||||
async def _fake_async_ask(question, kb_ids, uid, search_config=None):
|
||||
captured["question"] = question
|
||||
captured["kb_ids"] = kb_ids
|
||||
captured["uid"] = uid
|
||||
captured["search_config"] = search_config
|
||||
yield {"answer": "first"}
|
||||
raise RuntimeError("ask boom")
|
||||
|
||||
monkeypatch.setattr(module, "async_ask", _fake_async_ask)
|
||||
resp = _run(module.ask_about.__wrapped__())
|
||||
assert resp.headers["Content-Type"] == "text/event-stream; charset=utf-8"
|
||||
sse_text = _run(_read_sse_text(resp))
|
||||
assert '"answer": "first"' in sse_text
|
||||
assert "**ERROR**: ask boom" in sse_text
|
||||
assert '"data": true' in sse_text.lower()
|
||||
assert captured == {"question": "q", "kb_ids": ["kb-1"], "uid": "user-1", "search_config": {"mode": "test"}}
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_mindmap_and_related_questions_matrix_unit(monkeypatch):
|
||||
module = _load_conversation_module(monkeypatch)
|
||||
|
||||
def _search_detail(_sid):
|
||||
return {
|
||||
"tenant_id": "tenant-x",
|
||||
"search_config": {
|
||||
"kb_ids": ["kb-2", "kb-3"],
|
||||
"chat_id": "chat-x",
|
||||
"llm_setting": {"temperature": 0.2, "parameter": {"k": "v"}},
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(module.SearchService, "get_detail", _search_detail)
|
||||
|
||||
_set_request_json(monkeypatch, module, {"question": "mindmap-q", "kb_ids": ["kb-1", "kb-2"], "search_id": "search-1"})
|
||||
mindmap_calls = {}
|
||||
|
||||
async def _gen_ok(question, kb_ids, tenant_id, search_config):
|
||||
mindmap_calls["question"] = question
|
||||
mindmap_calls["kb_ids"] = set(kb_ids)
|
||||
mindmap_calls["tenant_id"] = tenant_id
|
||||
mindmap_calls["search_config"] = search_config
|
||||
return {"nodes": [question]}
|
||||
|
||||
monkeypatch.setattr(module, "gen_mindmap", _gen_ok)
|
||||
res = _run(module.mindmap.__wrapped__())
|
||||
assert res["code"] == 0
|
||||
assert res["data"] == {"nodes": ["mindmap-q"]}
|
||||
assert mindmap_calls["kb_ids"] == {"kb-1", "kb-2", "kb-3"}
|
||||
assert mindmap_calls["tenant_id"] == "tenant-x"
|
||||
assert set(mindmap_calls["search_config"]["kb_ids"]) == {"kb-1", "kb-2", "kb-3"}
|
||||
|
||||
async def _gen_error(*_args, **_kwargs):
|
||||
return {"error": "mindmap boom"}
|
||||
|
||||
monkeypatch.setattr(module, "gen_mindmap", _gen_error)
|
||||
res = _run(module.mindmap.__wrapped__())
|
||||
assert "mindmap boom" in res["message"]
|
||||
|
||||
llm_calls = {}
|
||||
|
||||
class _FakeChat:
|
||||
async def async_chat(self, prompt, messages, options):
|
||||
llm_calls["prompt"] = prompt
|
||||
llm_calls["messages"] = messages
|
||||
llm_calls["options"] = options
|
||||
return "1. Alpha\n2. Beta\nignored"
|
||||
|
||||
def _fake_bundle(tenant_id, model_config, lang="Chinese", **kwargs):
|
||||
llm_calls["bundle"] = (tenant_id, model_config)
|
||||
return _FakeChat()
|
||||
|
||||
monkeypatch.setattr(module, "LLMBundle", _fake_bundle)
|
||||
monkeypatch.setattr(module, "load_prompt", lambda name: f"prompt-{name}")
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"get_model_config_by_type_and_name",
|
||||
lambda *_args, **_kwargs: {"llm_factory": "test", "llm_name": "chat-x", "model_type": module.LLMType.CHAT.value},
|
||||
)
|
||||
_set_request_json(monkeypatch, module, {"question": "solar", "search_id": "search-1"})
|
||||
res = _run(module.related_questions.__wrapped__())
|
||||
assert res["code"] == 0
|
||||
assert res["data"] == ["Alpha", "Beta"]
|
||||
assert llm_calls["bundle"][0] == "user-1"
|
||||
assert llm_calls["options"] == {"temperature": 0.2}
|
||||
assert llm_calls["prompt"] == "prompt-related_question"
|
||||
assert "Keywords: solar" in llm_calls["messages"][0]["content"]
|
||||
Reference in New Issue
Block a user