from __future__ import annotations import pytest from yuxi.knowledge.implementations.dify import DifyKB class _FakeResponse: def __init__(self, payload: dict): self._payload = payload def raise_for_status(self) -> None: return None def json(self) -> dict: return self._payload class _FakeAsyncClient: def __init__(self, response_payload: dict | None = None, raises: Exception | None = None, **kwargs): del kwargs self._response_payload = response_payload or {} self._raises = raises async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): return False async def post(self, url: str, json: dict, headers: dict): assert "/datasets/" in url assert headers.get("Authorization", "").startswith("Bearer ") if self._raises: raise self._raises assert json["retrieval_model"]["search_method"] == "semantic_search" assert json["retrieval_model"]["top_k"] == 5 assert json["retrieval_model"]["reranking_enable"] is False assert json["retrieval_model"]["score_threshold_enabled"] is True assert json["retrieval_model"]["score_threshold"] == 0.3 return _FakeResponse(self._response_payload) def test_dify_create_params_config_and_validation(): config = DifyKB.get_create_params_config() keys = [option["key"] for option in config["options"]] assert keys == ["dify_api_url", "dify_token", "dify_dataset_id"] assert all(option["required"] for option in config["options"]) params = DifyKB.normalize_additional_params( { "dify_api_url": " https://api.dify.ai/v1 ", "dify_token": " token ", "dify_dataset_id": " dataset-123 ", } ) assert params == { "dify_api_url": "https://api.dify.ai/v1", "dify_token": "token", "dify_dataset_id": "dataset-123", } assert "chunk_preset_id" not in params def test_dify_validation_rejects_missing_or_invalid_params(): with pytest.raises(ValueError, match="Dify 参数缺失"): DifyKB.normalize_additional_params({"dify_api_url": "https://api.dify.ai/v1"}) with pytest.raises(ValueError, match="必须以 /v1 结尾"): DifyKB.normalize_additional_params( { "dify_api_url": "https://api.dify.ai", "dify_token": "token", "dify_dataset_id": "dataset-123", } ) @pytest.mark.asyncio async def test_dify_kb_aquery_maps_records(monkeypatch, tmp_path): kb = DifyKB(str(tmp_path)) slug = "kb_test_dify" kb.databases_meta[slug] = { "name": "dify-kb", "description": "test", "kb_type": "dify", "query_params": { "options": { "search_mode": "vector", "final_top_k": 5, "score_threshold_enabled": True, "similarity_threshold": 0.3, } }, "metadata": { "dify_api_url": "https://api.dify.ai/v1", "dify_token": "token", "dify_dataset_id": "dataset-123", }, } payload = { "records": [ { "score": 0.98, "segment": { "id": "seg-1", "position": 2, "content": "hello world", "document": {"id": "doc-1", "name": "Doc One"}, }, } ] } monkeypatch.setattr( "yuxi.knowledge.implementations.dify.httpx.AsyncClient", lambda **kwargs: _FakeAsyncClient(response_payload=payload, **kwargs), ) result = await kb.aquery("hello", slug) assert len(result) == 1 assert result[0]["content"] == "hello world" assert result[0]["score"] == 0.98 assert result[0]["metadata"]["source"] == "Doc One" assert result[0]["metadata"]["file_id"] == "doc-1" assert result[0]["metadata"]["chunk_id"] == "seg-1" assert result[0]["metadata"]["chunk_index"] == 2 @pytest.mark.asyncio async def test_dify_kb_aquery_error_returns_empty(monkeypatch, tmp_path): kb = DifyKB(str(tmp_path)) slug = "kb_test_dify_error" kb.databases_meta[slug] = { "name": "dify-kb", "description": "test", "kb_type": "dify", "query_params": {"options": {}}, "metadata": { "dify_api_url": "https://api.dify.ai/v1", "dify_token": "token", "dify_dataset_id": "dataset-123", }, } monkeypatch.setattr( "yuxi.knowledge.implementations.dify.httpx.AsyncClient", lambda **kwargs: _FakeAsyncClient(raises=RuntimeError("boom"), **kwargs), ) result = await kb.aquery("hello", slug) assert result == []