naas-abi-core 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assets/favicon.ico +0 -0
- assets/logo.png +0 -0
- naas_abi_core/__init__.py +1 -0
- naas_abi_core/apps/api/api.py +245 -0
- naas_abi_core/apps/api/api_test.py +281 -0
- naas_abi_core/apps/api/openapi_doc.py +144 -0
- naas_abi_core/apps/mcp/Dockerfile.mcp +35 -0
- naas_abi_core/apps/mcp/mcp_server.py +243 -0
- naas_abi_core/apps/mcp/mcp_server_test.py +163 -0
- naas_abi_core/apps/terminal_agent/main.py +555 -0
- naas_abi_core/apps/terminal_agent/terminal_style.py +175 -0
- naas_abi_core/engine/Engine.py +87 -0
- naas_abi_core/engine/EngineProxy.py +109 -0
- naas_abi_core/engine/Engine_test.py +6 -0
- naas_abi_core/engine/IEngine.py +91 -0
- naas_abi_core/engine/conftest.py +45 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration.py +216 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_Deploy.py +7 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_GenericLoader.py +49 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService.py +159 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService_test.py +26 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService.py +138 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService_test.py +74 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService.py +224 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService_test.py +109 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService.py +76 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService_test.py +33 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_test.py +9 -0
- naas_abi_core/engine/engine_configuration/utils/PydanticModelValidator.py +15 -0
- naas_abi_core/engine/engine_loaders/EngineModuleLoader.py +302 -0
- naas_abi_core/engine/engine_loaders/EngineOntologyLoader.py +16 -0
- naas_abi_core/engine/engine_loaders/EngineServiceLoader.py +47 -0
- naas_abi_core/integration/__init__.py +7 -0
- naas_abi_core/integration/integration.py +28 -0
- naas_abi_core/models/Model.py +198 -0
- naas_abi_core/models/OpenRouter.py +18 -0
- naas_abi_core/models/OpenRouter_test.py +36 -0
- naas_abi_core/module/Module.py +252 -0
- naas_abi_core/module/ModuleAgentLoader.py +50 -0
- naas_abi_core/module/ModuleUtils.py +20 -0
- naas_abi_core/modules/templatablesparqlquery/README.md +196 -0
- naas_abi_core/modules/templatablesparqlquery/__init__.py +39 -0
- naas_abi_core/modules/templatablesparqlquery/ontologies/TemplatableSparqlQueryOntology.ttl +116 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/GenericWorkflow.py +48 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/TemplatableSparqlQueryLoader.py +192 -0
- naas_abi_core/pipeline/__init__.py +6 -0
- naas_abi_core/pipeline/pipeline.py +70 -0
- naas_abi_core/services/__init__.py +0 -0
- naas_abi_core/services/agent/Agent.py +1619 -0
- naas_abi_core/services/agent/AgentMemory_test.py +28 -0
- naas_abi_core/services/agent/Agent_test.py +214 -0
- naas_abi_core/services/agent/IntentAgent.py +1179 -0
- naas_abi_core/services/agent/IntentAgent_test.py +139 -0
- naas_abi_core/services/agent/beta/Embeddings.py +181 -0
- naas_abi_core/services/agent/beta/IntentMapper.py +120 -0
- naas_abi_core/services/agent/beta/LocalModel.py +88 -0
- naas_abi_core/services/agent/beta/VectorStore.py +89 -0
- naas_abi_core/services/agent/test_agent_memory.py +278 -0
- naas_abi_core/services/agent/test_postgres_integration.py +145 -0
- naas_abi_core/services/cache/CacheFactory.py +31 -0
- naas_abi_core/services/cache/CachePort.py +63 -0
- naas_abi_core/services/cache/CacheService.py +246 -0
- naas_abi_core/services/cache/CacheService_test.py +85 -0
- naas_abi_core/services/cache/adapters/secondary/CacheFSAdapter.py +39 -0
- naas_abi_core/services/object_storage/ObjectStorageFactory.py +57 -0
- naas_abi_core/services/object_storage/ObjectStoragePort.py +47 -0
- naas_abi_core/services/object_storage/ObjectStorageService.py +41 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterFS.py +52 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterNaas.py +131 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterS3.py +171 -0
- naas_abi_core/services/ontology/OntologyPorts.py +36 -0
- naas_abi_core/services/ontology/OntologyService.py +17 -0
- naas_abi_core/services/ontology/adaptors/secondary/OntologyService_SecondaryAdaptor_NERPort.py +37 -0
- naas_abi_core/services/secret/Secret.py +138 -0
- naas_abi_core/services/secret/SecretPorts.py +45 -0
- naas_abi_core/services/secret/Secret_test.py +65 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret.py +57 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret_test.py +39 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret.py +88 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret_test.py +25 -0
- naas_abi_core/services/secret/adaptors/secondary/dotenv_secret_secondaryadaptor.py +29 -0
- naas_abi_core/services/triple_store/TripleStoreFactory.py +116 -0
- naas_abi_core/services/triple_store/TripleStorePorts.py +223 -0
- naas_abi_core/services/triple_store/TripleStoreService.py +419 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune.py +1300 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune_test.py +284 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph.py +597 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph_test.py +1474 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__Filesystem.py +223 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__ObjectStorage.py +234 -0
- naas_abi_core/services/triple_store/adaptors/secondary/base/TripleStoreService__SecondaryAdaptor__FileBase.py +18 -0
- naas_abi_core/services/vector_store/IVectorStorePort.py +101 -0
- naas_abi_core/services/vector_store/IVectorStorePort_test.py +189 -0
- naas_abi_core/services/vector_store/VectorStoreFactory.py +47 -0
- naas_abi_core/services/vector_store/VectorStoreService.py +171 -0
- naas_abi_core/services/vector_store/VectorStoreService_test.py +185 -0
- naas_abi_core/services/vector_store/__init__.py +13 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter.py +251 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter_test.py +57 -0
- naas_abi_core/tests/test_services_imports.py +69 -0
- naas_abi_core/utils/Expose.py +55 -0
- naas_abi_core/utils/Graph.py +182 -0
- naas_abi_core/utils/JSON.py +49 -0
- naas_abi_core/utils/LazyLoader.py +44 -0
- naas_abi_core/utils/Logger.py +12 -0
- naas_abi_core/utils/OntologyReasoner.py +141 -0
- naas_abi_core/utils/OntologyYaml.py +681 -0
- naas_abi_core/utils/SPARQL.py +256 -0
- naas_abi_core/utils/Storage.py +33 -0
- naas_abi_core/utils/StorageUtils.py +398 -0
- naas_abi_core/utils/String.py +52 -0
- naas_abi_core/utils/Workers.py +114 -0
- naas_abi_core/utils/__init__.py +0 -0
- naas_abi_core/utils/onto2py/README.md +0 -0
- naas_abi_core/utils/onto2py/__init__.py +10 -0
- naas_abi_core/utils/onto2py/__main__.py +29 -0
- naas_abi_core/utils/onto2py/onto2py.py +611 -0
- naas_abi_core/utils/onto2py/tests/ttl2py_test.py +271 -0
- naas_abi_core/workflow/__init__.py +5 -0
- naas_abi_core/workflow/workflow.py +48 -0
- naas_abi_core-1.4.1.dist-info/METADATA +630 -0
- naas_abi_core-1.4.1.dist-info/RECORD +124 -0
- naas_abi_core-1.4.1.dist-info/WHEEL +4 -0
- naas_abi_core-1.4.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import List
|
|
4
|
+
from abc import ABC
|
|
5
|
+
from .IVectorStorePort import IVectorStorePort, VectorDocument
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GenericVectorStoreAdapterTest(ABC):
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def adapter(self) -> IVectorStorePort:
|
|
11
|
+
raise NotImplementedError("Subclasses must provide an adapter fixture")
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def test_collection_name(self) -> str:
|
|
15
|
+
return "test_collection"
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def test_dimension(self) -> int:
|
|
19
|
+
return 128
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def sample_vectors(self) -> List[np.ndarray]:
|
|
23
|
+
np.random.seed(42)
|
|
24
|
+
return [
|
|
25
|
+
np.random.randn(128).astype(np.float32) for _ in range(5)
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def sample_documents(self, sample_vectors) -> List[VectorDocument]:
|
|
30
|
+
return [
|
|
31
|
+
VectorDocument(
|
|
32
|
+
id=f"doc_{i}",
|
|
33
|
+
vector=vector,
|
|
34
|
+
metadata={"category": f"cat_{i % 2}", "index": i},
|
|
35
|
+
payload={"data": f"payload_{i}"}
|
|
36
|
+
)
|
|
37
|
+
for i, vector in enumerate(sample_vectors)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def test_initialize(self, adapter):
|
|
41
|
+
adapter.initialize()
|
|
42
|
+
assert True
|
|
43
|
+
|
|
44
|
+
def test_create_and_list_collections(
|
|
45
|
+
self, adapter, test_collection_name, test_dimension
|
|
46
|
+
):
|
|
47
|
+
adapter.initialize()
|
|
48
|
+
|
|
49
|
+
adapter.create_collection(
|
|
50
|
+
test_collection_name, test_dimension, distance_metric="cosine"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
collections = adapter.list_collections()
|
|
54
|
+
assert test_collection_name in collections
|
|
55
|
+
|
|
56
|
+
def test_store_and_retrieve_vectors(
|
|
57
|
+
self, adapter, test_collection_name, test_dimension, sample_documents
|
|
58
|
+
):
|
|
59
|
+
adapter.initialize()
|
|
60
|
+
|
|
61
|
+
adapter.create_collection(test_collection_name, test_dimension)
|
|
62
|
+
|
|
63
|
+
adapter.store_vectors(test_collection_name, sample_documents)
|
|
64
|
+
|
|
65
|
+
retrieved = adapter.get_vector(
|
|
66
|
+
test_collection_name, "doc_0", include_vector=True
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert retrieved is not None
|
|
70
|
+
assert retrieved.id == "doc_0"
|
|
71
|
+
assert retrieved.metadata["category"] == "cat_0"
|
|
72
|
+
assert retrieved.payload["data"] == "payload_0"
|
|
73
|
+
assert retrieved.vector is not None
|
|
74
|
+
np.testing.assert_array_almost_equal(
|
|
75
|
+
retrieved.vector, sample_documents[0].vector, decimal=5
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def test_search_vectors(
|
|
79
|
+
self, adapter, test_collection_name, test_dimension, sample_documents
|
|
80
|
+
):
|
|
81
|
+
adapter.initialize()
|
|
82
|
+
|
|
83
|
+
adapter.create_collection(test_collection_name, test_dimension)
|
|
84
|
+
adapter.store_vectors(test_collection_name, sample_documents)
|
|
85
|
+
|
|
86
|
+
query_vector = sample_documents[0].vector
|
|
87
|
+
results = adapter.search(
|
|
88
|
+
test_collection_name,
|
|
89
|
+
query_vector,
|
|
90
|
+
k=3,
|
|
91
|
+
include_metadata=True
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert len(results) <= 3
|
|
95
|
+
assert results[0].id == "doc_0"
|
|
96
|
+
assert results[0].score >= 0.99
|
|
97
|
+
|
|
98
|
+
def test_search_with_filter(
|
|
99
|
+
self, adapter, test_collection_name, test_dimension, sample_documents
|
|
100
|
+
):
|
|
101
|
+
adapter.initialize()
|
|
102
|
+
|
|
103
|
+
adapter.create_collection(test_collection_name, test_dimension)
|
|
104
|
+
adapter.store_vectors(test_collection_name, sample_documents)
|
|
105
|
+
|
|
106
|
+
query_vector = sample_documents[0].vector
|
|
107
|
+
results = adapter.search(
|
|
108
|
+
test_collection_name,
|
|
109
|
+
query_vector,
|
|
110
|
+
k=10,
|
|
111
|
+
filter={"category": "cat_0"},
|
|
112
|
+
include_metadata=True
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
for result in results:
|
|
116
|
+
assert result.metadata["category"] == "cat_0"
|
|
117
|
+
|
|
118
|
+
def test_update_vector(
|
|
119
|
+
self, adapter, test_collection_name, test_dimension, sample_documents
|
|
120
|
+
):
|
|
121
|
+
adapter.initialize()
|
|
122
|
+
|
|
123
|
+
adapter.create_collection(test_collection_name, test_dimension)
|
|
124
|
+
adapter.store_vectors(test_collection_name, [sample_documents[0]])
|
|
125
|
+
|
|
126
|
+
new_metadata = {"category": "updated", "new_field": "value"}
|
|
127
|
+
adapter.update_vector(
|
|
128
|
+
test_collection_name,
|
|
129
|
+
"doc_0",
|
|
130
|
+
metadata=new_metadata
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
retrieved = adapter.get_vector(test_collection_name, "doc_0")
|
|
134
|
+
assert retrieved.metadata["category"] == "updated"
|
|
135
|
+
assert retrieved.metadata["new_field"] == "value"
|
|
136
|
+
|
|
137
|
+
def test_delete_vectors(
|
|
138
|
+
self, adapter, test_collection_name, test_dimension, sample_documents
|
|
139
|
+
):
|
|
140
|
+
adapter.initialize()
|
|
141
|
+
|
|
142
|
+
adapter.create_collection(test_collection_name, test_dimension)
|
|
143
|
+
adapter.store_vectors(test_collection_name, sample_documents)
|
|
144
|
+
|
|
145
|
+
initial_count = adapter.count_vectors(test_collection_name)
|
|
146
|
+
assert initial_count == len(sample_documents)
|
|
147
|
+
|
|
148
|
+
adapter.delete_vectors(test_collection_name, ["doc_0", "doc_1"])
|
|
149
|
+
|
|
150
|
+
final_count = adapter.count_vectors(test_collection_name)
|
|
151
|
+
assert final_count == initial_count - 2
|
|
152
|
+
|
|
153
|
+
deleted_doc = adapter.get_vector(test_collection_name, "doc_0")
|
|
154
|
+
assert deleted_doc is None
|
|
155
|
+
|
|
156
|
+
def test_count_vectors(
|
|
157
|
+
self, adapter, test_collection_name, test_dimension, sample_documents
|
|
158
|
+
):
|
|
159
|
+
adapter.initialize()
|
|
160
|
+
|
|
161
|
+
adapter.create_collection(test_collection_name, test_dimension)
|
|
162
|
+
|
|
163
|
+
count_empty = adapter.count_vectors(test_collection_name)
|
|
164
|
+
assert count_empty == 0
|
|
165
|
+
|
|
166
|
+
adapter.store_vectors(test_collection_name, sample_documents)
|
|
167
|
+
|
|
168
|
+
count_filled = adapter.count_vectors(test_collection_name)
|
|
169
|
+
assert count_filled == len(sample_documents)
|
|
170
|
+
|
|
171
|
+
def test_delete_collection(
|
|
172
|
+
self, adapter, test_collection_name, test_dimension
|
|
173
|
+
):
|
|
174
|
+
adapter.initialize()
|
|
175
|
+
|
|
176
|
+
adapter.create_collection(test_collection_name, test_dimension)
|
|
177
|
+
|
|
178
|
+
collections_before = adapter.list_collections()
|
|
179
|
+
assert test_collection_name in collections_before
|
|
180
|
+
|
|
181
|
+
adapter.delete_collection(test_collection_name)
|
|
182
|
+
|
|
183
|
+
collections_after = adapter.list_collections()
|
|
184
|
+
assert test_collection_name not in collections_after
|
|
185
|
+
|
|
186
|
+
def test_close(self, adapter):
|
|
187
|
+
adapter.initialize()
|
|
188
|
+
adapter.close()
|
|
189
|
+
assert True
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .IVectorStorePort import IVectorStorePort
|
|
6
|
+
from .VectorStoreService import VectorStoreService
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VectorStoreFactory:
|
|
12
|
+
_instance: Optional[VectorStoreService] = None
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def create_adapter(cls) -> IVectorStorePort:
|
|
16
|
+
adapter_type = os.getenv("VECTOR_STORE_ADAPTER", "qdrant").lower()
|
|
17
|
+
|
|
18
|
+
if adapter_type == "qdrant":
|
|
19
|
+
# Lazy import to avoid loading qdrant_client at module import time
|
|
20
|
+
from .adapters.QdrantAdapter import QdrantAdapter
|
|
21
|
+
|
|
22
|
+
host = os.getenv("QDRANT_HOST", "localhost")
|
|
23
|
+
port = int(os.getenv("QDRANT_PORT", "6333"))
|
|
24
|
+
api_key = os.getenv("QDRANT_API_KEY")
|
|
25
|
+
https = os.getenv("QDRANT_HTTPS", "false").lower() == "true"
|
|
26
|
+
timeout = int(os.getenv("QDRANT_TIMEOUT", "30"))
|
|
27
|
+
|
|
28
|
+
logger.info(f"Creating Qdrant adapter (host={host}, port={port})")
|
|
29
|
+
return QdrantAdapter(
|
|
30
|
+
host=host, port=port, api_key=api_key, https=https, timeout=timeout
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
raise ValueError(f"Unknown vector store adapter: {adapter_type}")
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_service(cls) -> VectorStoreService:
|
|
37
|
+
if cls._instance is None:
|
|
38
|
+
adapter = cls.create_adapter()
|
|
39
|
+
cls._instance = VectorStoreService(adapter)
|
|
40
|
+
logger.info("VectorStoreService singleton created")
|
|
41
|
+
return cls._instance
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def reset(cls) -> None:
|
|
45
|
+
if cls._instance:
|
|
46
|
+
logger.info("Resetting VectorStoreService singleton")
|
|
47
|
+
cls._instance = None
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import List, Dict, Any, Optional
|
|
3
|
+
import numpy as np
|
|
4
|
+
from .IVectorStorePort import IVectorStorePort, VectorDocument, SearchResult
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VectorStoreService:
|
|
10
|
+
def __init__(self, adapter: IVectorStorePort):
|
|
11
|
+
self.adapter = adapter
|
|
12
|
+
self._initialized = False
|
|
13
|
+
|
|
14
|
+
def initialize(self) -> None:
|
|
15
|
+
if not self._initialized:
|
|
16
|
+
self.adapter.initialize()
|
|
17
|
+
self._initialized = True
|
|
18
|
+
logger.info("VectorStoreService initialized successfully")
|
|
19
|
+
|
|
20
|
+
def ensure_collection(
|
|
21
|
+
self,
|
|
22
|
+
collection_name: str,
|
|
23
|
+
dimension: int,
|
|
24
|
+
distance_metric: str = "cosine",
|
|
25
|
+
recreate: bool = False,
|
|
26
|
+
**kwargs
|
|
27
|
+
) -> None:
|
|
28
|
+
self.initialize()
|
|
29
|
+
|
|
30
|
+
existing_collections = self.adapter.list_collections()
|
|
31
|
+
|
|
32
|
+
if collection_name in existing_collections:
|
|
33
|
+
if recreate:
|
|
34
|
+
logger.info(f"Recreating collection: {collection_name}")
|
|
35
|
+
self.adapter.delete_collection(collection_name)
|
|
36
|
+
self.adapter.create_collection(
|
|
37
|
+
collection_name, dimension, distance_metric, **kwargs
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
logger.debug(f"Collection {collection_name} already exists")
|
|
41
|
+
else:
|
|
42
|
+
logger.info(f"Creating new collection: {collection_name}")
|
|
43
|
+
self.adapter.create_collection(
|
|
44
|
+
collection_name, dimension, distance_metric, **kwargs
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def add_documents(
|
|
48
|
+
self,
|
|
49
|
+
collection_name: str,
|
|
50
|
+
ids: List[str],
|
|
51
|
+
vectors: List[np.ndarray],
|
|
52
|
+
metadata: Optional[List[Dict[str, Any]]] = None,
|
|
53
|
+
payloads: Optional[List[Dict[str, Any]]] = None
|
|
54
|
+
) -> None:
|
|
55
|
+
self.initialize()
|
|
56
|
+
|
|
57
|
+
if not ids or not vectors:
|
|
58
|
+
raise ValueError("IDs and vectors cannot be empty")
|
|
59
|
+
|
|
60
|
+
if len(ids) != len(vectors):
|
|
61
|
+
raise ValueError("Number of IDs must match number of vectors")
|
|
62
|
+
|
|
63
|
+
if metadata and len(metadata) != len(ids):
|
|
64
|
+
raise ValueError("Number of metadata entries must match number of IDs")
|
|
65
|
+
|
|
66
|
+
if payloads and len(payloads) != len(ids):
|
|
67
|
+
raise ValueError("Number of payloads must match number of IDs")
|
|
68
|
+
|
|
69
|
+
documents = []
|
|
70
|
+
for i, (doc_id, vector) in enumerate(zip(ids, vectors)):
|
|
71
|
+
doc = VectorDocument(
|
|
72
|
+
id=doc_id,
|
|
73
|
+
vector=vector,
|
|
74
|
+
metadata=metadata[i] if metadata else {},
|
|
75
|
+
payload=payloads[i] if payloads else None
|
|
76
|
+
)
|
|
77
|
+
documents.append(doc)
|
|
78
|
+
|
|
79
|
+
self.adapter.store_vectors(collection_name, documents)
|
|
80
|
+
logger.debug(f"Added {len(documents)} documents to collection {collection_name}")
|
|
81
|
+
|
|
82
|
+
def search_similar(
|
|
83
|
+
self,
|
|
84
|
+
collection_name: str,
|
|
85
|
+
query_vector: np.ndarray,
|
|
86
|
+
k: int = 10,
|
|
87
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
88
|
+
score_threshold: Optional[float] = None,
|
|
89
|
+
include_vectors: bool = False,
|
|
90
|
+
include_metadata: bool = True
|
|
91
|
+
) -> List[SearchResult]:
|
|
92
|
+
self.initialize()
|
|
93
|
+
|
|
94
|
+
if k <= 0:
|
|
95
|
+
raise ValueError("k must be a positive integer")
|
|
96
|
+
|
|
97
|
+
results = self.adapter.search(
|
|
98
|
+
collection_name=collection_name,
|
|
99
|
+
query_vector=query_vector,
|
|
100
|
+
k=k,
|
|
101
|
+
filter=filter,
|
|
102
|
+
include_vectors=include_vectors,
|
|
103
|
+
include_metadata=include_metadata
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if score_threshold is not None:
|
|
107
|
+
results = [r for r in results if r.score >= score_threshold]
|
|
108
|
+
|
|
109
|
+
logger.debug(f"Found {len(results)} similar vectors in {collection_name}")
|
|
110
|
+
return results
|
|
111
|
+
|
|
112
|
+
def get_document(
|
|
113
|
+
self,
|
|
114
|
+
collection_name: str,
|
|
115
|
+
document_id: str,
|
|
116
|
+
include_vector: bool = True
|
|
117
|
+
) -> Optional[VectorDocument]:
|
|
118
|
+
self.initialize()
|
|
119
|
+
return self.adapter.get_vector(
|
|
120
|
+
collection_name, document_id, include_vector
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def update_document(
|
|
124
|
+
self,
|
|
125
|
+
collection_name: str,
|
|
126
|
+
document_id: str,
|
|
127
|
+
vector: Optional[np.ndarray] = None,
|
|
128
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
129
|
+
payload: Optional[Dict[str, Any]] = None
|
|
130
|
+
) -> None:
|
|
131
|
+
self.initialize()
|
|
132
|
+
|
|
133
|
+
if vector is None and metadata is None and payload is None:
|
|
134
|
+
raise ValueError("At least one of vector, metadata, or payload must be provided")
|
|
135
|
+
|
|
136
|
+
self.adapter.update_vector(
|
|
137
|
+
collection_name, document_id, vector, metadata, payload
|
|
138
|
+
)
|
|
139
|
+
logger.debug(f"Updated document {document_id} in collection {collection_name}")
|
|
140
|
+
|
|
141
|
+
def delete_documents(
|
|
142
|
+
self,
|
|
143
|
+
collection_name: str,
|
|
144
|
+
document_ids: List[str]
|
|
145
|
+
) -> None:
|
|
146
|
+
self.initialize()
|
|
147
|
+
|
|
148
|
+
if not document_ids:
|
|
149
|
+
raise ValueError("Document IDs cannot be empty")
|
|
150
|
+
|
|
151
|
+
self.adapter.delete_vectors(collection_name, document_ids)
|
|
152
|
+
logger.info(f"Deleted {len(document_ids)} documents from collection {collection_name}")
|
|
153
|
+
|
|
154
|
+
def get_collection_size(self, collection_name: str) -> int:
|
|
155
|
+
self.initialize()
|
|
156
|
+
return self.adapter.count_vectors(collection_name)
|
|
157
|
+
|
|
158
|
+
def list_collections(self) -> List[str]:
|
|
159
|
+
self.initialize()
|
|
160
|
+
return self.adapter.list_collections()
|
|
161
|
+
|
|
162
|
+
def delete_collection(self, collection_name: str) -> None:
|
|
163
|
+
self.initialize()
|
|
164
|
+
self.adapter.delete_collection(collection_name)
|
|
165
|
+
logger.info(f"Deleted collection: {collection_name}")
|
|
166
|
+
|
|
167
|
+
def close(self) -> None:
|
|
168
|
+
if self._initialized:
|
|
169
|
+
self.adapter.close()
|
|
170
|
+
self._initialized = False
|
|
171
|
+
logger.info("VectorStoreService closed")
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import Mock
|
|
3
|
+
import numpy as np
|
|
4
|
+
from .VectorStoreService import VectorStoreService
|
|
5
|
+
from .IVectorStorePort import IVectorStorePort, VectorDocument, SearchResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestVectorStoreService:
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_adapter(self):
|
|
11
|
+
adapter = Mock(spec=IVectorStorePort)
|
|
12
|
+
adapter.initialize = Mock()
|
|
13
|
+
adapter.list_collections = Mock(return_value=[])
|
|
14
|
+
adapter.create_collection = Mock()
|
|
15
|
+
adapter.delete_collection = Mock()
|
|
16
|
+
adapter.store_vectors = Mock()
|
|
17
|
+
adapter.search = Mock()
|
|
18
|
+
adapter.get_vector = Mock()
|
|
19
|
+
adapter.update_vector = Mock()
|
|
20
|
+
adapter.delete_vectors = Mock()
|
|
21
|
+
adapter.count_vectors = Mock()
|
|
22
|
+
adapter.close = Mock()
|
|
23
|
+
return adapter
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def service(self, mock_adapter):
|
|
27
|
+
return VectorStoreService(mock_adapter)
|
|
28
|
+
|
|
29
|
+
def test_initialize(self, service, mock_adapter):
|
|
30
|
+
service.initialize()
|
|
31
|
+
mock_adapter.initialize.assert_called_once()
|
|
32
|
+
|
|
33
|
+
service.initialize()
|
|
34
|
+
mock_adapter.initialize.assert_called_once()
|
|
35
|
+
|
|
36
|
+
def test_ensure_collection_create_new(self, service, mock_adapter):
|
|
37
|
+
mock_adapter.list_collections.return_value = []
|
|
38
|
+
|
|
39
|
+
service.ensure_collection("test_collection", 128)
|
|
40
|
+
|
|
41
|
+
mock_adapter.create_collection.assert_called_once_with(
|
|
42
|
+
"test_collection", 128, "cosine"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def test_ensure_collection_exists_no_recreate(self, service, mock_adapter):
|
|
46
|
+
mock_adapter.list_collections.return_value = ["test_collection"]
|
|
47
|
+
|
|
48
|
+
service.ensure_collection("test_collection", 128, recreate=False)
|
|
49
|
+
|
|
50
|
+
mock_adapter.create_collection.assert_not_called()
|
|
51
|
+
mock_adapter.delete_collection.assert_not_called()
|
|
52
|
+
|
|
53
|
+
def test_ensure_collection_recreate(self, service, mock_adapter):
|
|
54
|
+
mock_adapter.list_collections.return_value = ["test_collection"]
|
|
55
|
+
|
|
56
|
+
service.ensure_collection("test_collection", 128, recreate=True)
|
|
57
|
+
|
|
58
|
+
mock_adapter.delete_collection.assert_called_once_with("test_collection")
|
|
59
|
+
mock_adapter.create_collection.assert_called_once_with(
|
|
60
|
+
"test_collection", 128, "cosine"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def test_add_documents(self, service, mock_adapter):
|
|
64
|
+
ids = ["doc1", "doc2"]
|
|
65
|
+
vectors = [np.random.randn(128), np.random.randn(128)]
|
|
66
|
+
metadata = [{"key": "value1"}, {"key": "value2"}]
|
|
67
|
+
|
|
68
|
+
service.add_documents("test_collection", ids, vectors, metadata)
|
|
69
|
+
|
|
70
|
+
mock_adapter.store_vectors.assert_called_once()
|
|
71
|
+
call_args = mock_adapter.store_vectors.call_args
|
|
72
|
+
assert call_args[0][0] == "test_collection"
|
|
73
|
+
assert len(call_args[0][1]) == 2
|
|
74
|
+
assert call_args[0][1][0].id == "doc1"
|
|
75
|
+
|
|
76
|
+
def test_add_documents_validation(self, service, mock_adapter):
|
|
77
|
+
with pytest.raises(ValueError, match="IDs and vectors cannot be empty"):
|
|
78
|
+
service.add_documents("test_collection", [], [])
|
|
79
|
+
|
|
80
|
+
with pytest.raises(ValueError, match="Number of IDs must match"):
|
|
81
|
+
service.add_documents(
|
|
82
|
+
"test_collection",
|
|
83
|
+
["doc1"],
|
|
84
|
+
[np.random.randn(128), np.random.randn(128)]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def test_search_similar(self, service, mock_adapter):
|
|
88
|
+
mock_results = [
|
|
89
|
+
SearchResult(id="doc1", score=0.95, metadata={"key": "value1"}),
|
|
90
|
+
SearchResult(id="doc2", score=0.85, metadata={"key": "value2"}),
|
|
91
|
+
]
|
|
92
|
+
mock_adapter.search.return_value = mock_results
|
|
93
|
+
|
|
94
|
+
query_vector = np.random.randn(128)
|
|
95
|
+
results = service.search_similar(
|
|
96
|
+
"test_collection", query_vector, k=5
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
assert len(results) == 2
|
|
100
|
+
assert results[0].id == "doc1"
|
|
101
|
+
mock_adapter.search.assert_called_once()
|
|
102
|
+
|
|
103
|
+
def test_search_similar_with_threshold(self, service, mock_adapter):
|
|
104
|
+
mock_results = [
|
|
105
|
+
SearchResult(id="doc1", score=0.95),
|
|
106
|
+
SearchResult(id="doc2", score=0.85),
|
|
107
|
+
SearchResult(id="doc3", score=0.75),
|
|
108
|
+
]
|
|
109
|
+
mock_adapter.search.return_value = mock_results
|
|
110
|
+
|
|
111
|
+
query_vector = np.random.randn(128)
|
|
112
|
+
results = service.search_similar(
|
|
113
|
+
"test_collection", query_vector, k=5, score_threshold=0.8
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert len(results) == 2
|
|
117
|
+
assert all(r.score >= 0.8 for r in results)
|
|
118
|
+
|
|
119
|
+
def test_get_document(self, service, mock_adapter):
|
|
120
|
+
mock_doc = VectorDocument(
|
|
121
|
+
id="doc1",
|
|
122
|
+
vector=np.random.randn(128),
|
|
123
|
+
metadata={"key": "value"}
|
|
124
|
+
)
|
|
125
|
+
mock_adapter.get_vector.return_value = mock_doc
|
|
126
|
+
|
|
127
|
+
doc = service.get_document("test_collection", "doc1")
|
|
128
|
+
|
|
129
|
+
assert doc.id == "doc1"
|
|
130
|
+
mock_adapter.get_vector.assert_called_once_with(
|
|
131
|
+
"test_collection", "doc1", True
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def test_update_document(self, service, mock_adapter):
|
|
135
|
+
new_metadata = {"key": "new_value"}
|
|
136
|
+
|
|
137
|
+
service.update_document(
|
|
138
|
+
"test_collection", "doc1", metadata=new_metadata
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
mock_adapter.update_vector.assert_called_once_with(
|
|
142
|
+
"test_collection", "doc1", None, new_metadata, None
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def test_update_document_validation(self, service, mock_adapter):
|
|
146
|
+
with pytest.raises(ValueError, match="At least one of"):
|
|
147
|
+
service.update_document("test_collection", "doc1")
|
|
148
|
+
|
|
149
|
+
def test_delete_documents(self, service, mock_adapter):
|
|
150
|
+
service.delete_documents("test_collection", ["doc1", "doc2"])
|
|
151
|
+
|
|
152
|
+
mock_adapter.delete_vectors.assert_called_once_with(
|
|
153
|
+
"test_collection", ["doc1", "doc2"]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def test_delete_documents_validation(self, service, mock_adapter):
|
|
157
|
+
with pytest.raises(ValueError, match="Document IDs cannot be empty"):
|
|
158
|
+
service.delete_documents("test_collection", [])
|
|
159
|
+
|
|
160
|
+
def test_get_collection_size(self, service, mock_adapter):
|
|
161
|
+
mock_adapter.count_vectors.return_value = 42
|
|
162
|
+
|
|
163
|
+
count = service.get_collection_size("test_collection")
|
|
164
|
+
|
|
165
|
+
assert count == 42
|
|
166
|
+
mock_adapter.count_vectors.assert_called_once_with("test_collection")
|
|
167
|
+
|
|
168
|
+
def test_list_collections(self, service, mock_adapter):
|
|
169
|
+
mock_adapter.list_collections.return_value = ["coll1", "coll2"]
|
|
170
|
+
|
|
171
|
+
collections = service.list_collections()
|
|
172
|
+
|
|
173
|
+
assert collections == ["coll1", "coll2"]
|
|
174
|
+
|
|
175
|
+
def test_delete_collection(self, service, mock_adapter):
|
|
176
|
+
service.delete_collection("test_collection")
|
|
177
|
+
|
|
178
|
+
mock_adapter.delete_collection.assert_called_once_with("test_collection")
|
|
179
|
+
|
|
180
|
+
def test_close(self, service, mock_adapter):
|
|
181
|
+
service.initialize()
|
|
182
|
+
service.close()
|
|
183
|
+
|
|
184
|
+
mock_adapter.close.assert_called_once()
|
|
185
|
+
assert not service._initialized
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# from .IVectorStorePort import IVectorStorePort, VectorDocument, SearchResult
|
|
2
|
+
# from .VectorStoreService import VectorStoreService
|
|
3
|
+
# from .VectorStoreFactory import VectorStoreFactory
|
|
4
|
+
# from .adapters.QdrantAdapter import QdrantAdapter
|
|
5
|
+
|
|
6
|
+
# __all__ = [
|
|
7
|
+
# "IVectorStorePort",
|
|
8
|
+
# "VectorDocument",
|
|
9
|
+
# "SearchResult",
|
|
10
|
+
# "VectorStoreService",
|
|
11
|
+
# "VectorStoreFactory",
|
|
12
|
+
# "QdrantAdapter",
|
|
13
|
+
# ]
|