test: 补充图谱抽取器、metadata 持久化与检索元数据测试;更新 roadmap

- LLM 抽取器测试:禁止自定义 Prompt、Schema 拼接、配置锁定修改
- metadata 持久化测试:验证 _save_metadata 不覆写数据库已有图谱配置
- 检索测试:验证 resource_id 写入 metadata
- roadmap 补充图谱抽取器配置优化条目
This commit is contained in:
Wenjie Zhang 2026-05-17 19:55:40 +08:00
parent 1344ae3f69
commit 5634ac3e27
3 changed files with 149 additions and 6 deletions

View File

@ -1,11 +1,11 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import MagicMock
from unittest.mock import AsyncMock, MagicMock
import pytest
from yuxi.knowledge.graphs.extractors import normalize_extraction_result
from yuxi.knowledge.graphs.extractors import LLMGraphExtractor, normalize_extraction_result
from yuxi.knowledge.graphs.milvus_graph_service import MilvusGraphService
@ -63,6 +63,70 @@ def test_normalize_extraction_result_rejects_invalid_payload(payload):
normalize_extraction_result(payload, "llm")
def test_llm_graph_extractor_rejects_custom_prompt():
extractor = LLMGraphExtractor({"model_spec": "test/model", "prompt": "custom"})
with pytest.raises(ValueError, match="不支持自定义完整 Prompt"):
extractor.validate_options()
def test_llm_graph_extractor_appends_schema_to_fixed_prompt():
extractor = LLMGraphExtractor(
{
"model_spec": "test/model",
"schema": "实体类型只能是 Person 或 Organization",
"concurrency_count": 5,
"model_params": {"temperature": 0.1},
}
)
prompt = extractor._build_prompt("张三任职于公司")
assert "请从下面文本中抽取实体和实体关系" in prompt
assert "抽取 Schema 约束" in prompt
assert "实体类型只能是 Person 或 Organization" in prompt
assert "文本:\n张三任职于公司" in prompt
@pytest.mark.asyncio
async def test_milvus_graph_service_configure_persists_updated_concurrency():
kb = SimpleNamespace(
kb_type="milvus",
additional_params={
"graph_build_config": {
"locked": True,
"extractor_type": "llm",
"extractor_options": {"model_spec": "test/model", "concurrency_count": 5},
}
},
)
class Repo:
async def get_by_id(self, db_id):
return kb
async def update(self, db_id, data):
kb.additional_params = data["additional_params"]
return kb
chunk_repo = SimpleNamespace(
count_by_db_id=AsyncMock(return_value=0),
count_graph_pending_by_db_id=AsyncMock(return_value=0),
count_graph_indexed_by_db_id=AsyncMock(return_value=0),
)
service = MilvusGraphService(kb_repo=Repo(), chunk_repo=chunk_repo)
await service.configure(
"kb_test",
extractor_type="llm",
extractor_options={"model_spec": "test/model", "concurrency_count": 9},
created_by="user_1",
)
status = await service.get_status("kb_test")
assert status["config"]["extractor_options"]["concurrency_count"] == 9
def test_milvus_graph_service_writes_chunk_entity_and_relation():
tx = MagicMock()
session = MagicMock()
@ -82,7 +146,7 @@ def test_milvus_graph_service_writes_chunk_entity_and_relation():
end_char_pos=8,
)
ent_ids, tags = service.write_chunk_graph(
entities, triples = service.write_chunk_graph(
"kb_test",
chunk,
normalize_extraction_result(
@ -104,8 +168,9 @@ def test_milvus_graph_service_writes_chunk_entity_and_relation():
),
)
assert ent_ids == ["张三", "公司"]
assert tags == ["Organization", "Person"]
assert [entity["name"] for entity in entities] == ["张三", "公司"]
assert {entity["label"] for entity in entities} == {"Person", "Organization"}
assert triples[0]["relation_type"] == "WORKS_AT"
queries = [call.args[0] for call in tx.run.call_args_list]
assert any("MERGE (c:Chunk:MilvusKB:`kb_test`" in query for query in queries)
assert any("MERGE (e:Entity:MilvusKB:`kb_test`" in query for query in queries)
@ -135,4 +200,4 @@ def test_milvus_graph_service_get_stats_empty_db_id():
import asyncio
result = asyncio.get_event_loop().run_until_complete(service.get_stats(db_id=None))
assert result == {"total_nodes": 0, "total_edges": 0, "entity_types": []}
assert result == {"total_nodes": 0, "total_edges": 0, "entity_types": []}

View File

@ -1,3 +1,4 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@ -164,3 +165,77 @@ class TestAddFileRecordSizeFallback:
metadata = await kb.add_file_record("db1", item, params=params)
assert metadata.get("size") is None
@pytest.mark.unit
@pytest.mark.asyncio
async def test_save_metadata_does_not_overwrite_existing_kb_config():
from yuxi.knowledge.base import KnowledgeBase
class TestKB(KnowledgeBase):
@property
def kb_type(self):
return "test"
async def _create_kb_instance(self, db_id, config):
pass
async def _initialize_kb_instance(self, instance):
pass
async def index_file(self, db_id, file_id, operator_id=None):
return {}
async def update_content(self, db_id, file_ids, params=None):
return []
async def aquery(self, query_text, db_id, **kwargs):
return []
def get_query_params_config(self, db_id, **kwargs):
return {"type": "test", "options": []}
async def delete_file(self, db_id, file_id):
pass
async def get_file_basic_info(self, db_id, file_id):
return {}
async def get_file_content(self, db_id, file_id):
return {}
async def get_file_info(self, db_id, file_id):
return {}
class ExistingKbRepo:
def __init__(self):
self.created = []
self.updated = []
async def get_by_id(self, db_id):
return SimpleNamespace(db_id=db_id)
async def create(self, payload):
self.created.append(payload)
async def update(self, db_id, data):
self.updated.append((db_id, data))
kb_repo = ExistingKbRepo()
kb = TestKB(work_dir="/tmp/test_kb")
kb.databases_meta["db1"] = {
"name": "Runtime name",
"description": "Runtime description",
"kb_type": "test",
"metadata": {"graph_build_config": {"extractor_options": {"concurrency_count": 5}}},
}
with (
patch("yuxi.repositories.knowledge_base_repository.KnowledgeBaseRepository", return_value=kb_repo),
patch("yuxi.repositories.knowledge_file_repository.KnowledgeFileRepository", return_value=SimpleNamespace()),
patch("yuxi.repositories.evaluation_repository.EvaluationRepository", return_value=SimpleNamespace()),
):
await kb._save_metadata()
assert kb_repo.created == []
assert kb_repo.updated == []

View File

@ -101,6 +101,7 @@ async def test_query_kb_returns_milvus_chunks_without_sandbox_paths(monkeypatch)
"metadata": {
"file_id": "file-1",
"source": "auth-guide.pdf",
"resource_id": "db-1",
},
}
]
@ -150,6 +151,7 @@ async def test_query_kb_allows_dify_knowledge_base(monkeypatch) -> None:
"metadata": {
"file_id": "dify-doc-1",
"source": "Dify Doc",
"resource_id": "db-1",
},
}
]
@ -219,6 +221,7 @@ async def test_query_kb_normalizes_file_metadata_for_open(monkeypatch) -> None:
assert result[0]["metadata"] == {
"file_id": "file-1",
"resource_id": "db-1",
"chunk_id": "chunk-1",
"chunk_index": 3,
}