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.
Files changed (124) hide show
  1. assets/favicon.ico +0 -0
  2. assets/logo.png +0 -0
  3. naas_abi_core/__init__.py +1 -0
  4. naas_abi_core/apps/api/api.py +245 -0
  5. naas_abi_core/apps/api/api_test.py +281 -0
  6. naas_abi_core/apps/api/openapi_doc.py +144 -0
  7. naas_abi_core/apps/mcp/Dockerfile.mcp +35 -0
  8. naas_abi_core/apps/mcp/mcp_server.py +243 -0
  9. naas_abi_core/apps/mcp/mcp_server_test.py +163 -0
  10. naas_abi_core/apps/terminal_agent/main.py +555 -0
  11. naas_abi_core/apps/terminal_agent/terminal_style.py +175 -0
  12. naas_abi_core/engine/Engine.py +87 -0
  13. naas_abi_core/engine/EngineProxy.py +109 -0
  14. naas_abi_core/engine/Engine_test.py +6 -0
  15. naas_abi_core/engine/IEngine.py +91 -0
  16. naas_abi_core/engine/conftest.py +45 -0
  17. naas_abi_core/engine/engine_configuration/EngineConfiguration.py +216 -0
  18. naas_abi_core/engine/engine_configuration/EngineConfiguration_Deploy.py +7 -0
  19. naas_abi_core/engine/engine_configuration/EngineConfiguration_GenericLoader.py +49 -0
  20. naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService.py +159 -0
  21. naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService_test.py +26 -0
  22. naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService.py +138 -0
  23. naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService_test.py +74 -0
  24. naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService.py +224 -0
  25. naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService_test.py +109 -0
  26. naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService.py +76 -0
  27. naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService_test.py +33 -0
  28. naas_abi_core/engine/engine_configuration/EngineConfiguration_test.py +9 -0
  29. naas_abi_core/engine/engine_configuration/utils/PydanticModelValidator.py +15 -0
  30. naas_abi_core/engine/engine_loaders/EngineModuleLoader.py +302 -0
  31. naas_abi_core/engine/engine_loaders/EngineOntologyLoader.py +16 -0
  32. naas_abi_core/engine/engine_loaders/EngineServiceLoader.py +47 -0
  33. naas_abi_core/integration/__init__.py +7 -0
  34. naas_abi_core/integration/integration.py +28 -0
  35. naas_abi_core/models/Model.py +198 -0
  36. naas_abi_core/models/OpenRouter.py +18 -0
  37. naas_abi_core/models/OpenRouter_test.py +36 -0
  38. naas_abi_core/module/Module.py +252 -0
  39. naas_abi_core/module/ModuleAgentLoader.py +50 -0
  40. naas_abi_core/module/ModuleUtils.py +20 -0
  41. naas_abi_core/modules/templatablesparqlquery/README.md +196 -0
  42. naas_abi_core/modules/templatablesparqlquery/__init__.py +39 -0
  43. naas_abi_core/modules/templatablesparqlquery/ontologies/TemplatableSparqlQueryOntology.ttl +116 -0
  44. naas_abi_core/modules/templatablesparqlquery/workflows/GenericWorkflow.py +48 -0
  45. naas_abi_core/modules/templatablesparqlquery/workflows/TemplatableSparqlQueryLoader.py +192 -0
  46. naas_abi_core/pipeline/__init__.py +6 -0
  47. naas_abi_core/pipeline/pipeline.py +70 -0
  48. naas_abi_core/services/__init__.py +0 -0
  49. naas_abi_core/services/agent/Agent.py +1619 -0
  50. naas_abi_core/services/agent/AgentMemory_test.py +28 -0
  51. naas_abi_core/services/agent/Agent_test.py +214 -0
  52. naas_abi_core/services/agent/IntentAgent.py +1179 -0
  53. naas_abi_core/services/agent/IntentAgent_test.py +139 -0
  54. naas_abi_core/services/agent/beta/Embeddings.py +181 -0
  55. naas_abi_core/services/agent/beta/IntentMapper.py +120 -0
  56. naas_abi_core/services/agent/beta/LocalModel.py +88 -0
  57. naas_abi_core/services/agent/beta/VectorStore.py +89 -0
  58. naas_abi_core/services/agent/test_agent_memory.py +278 -0
  59. naas_abi_core/services/agent/test_postgres_integration.py +145 -0
  60. naas_abi_core/services/cache/CacheFactory.py +31 -0
  61. naas_abi_core/services/cache/CachePort.py +63 -0
  62. naas_abi_core/services/cache/CacheService.py +246 -0
  63. naas_abi_core/services/cache/CacheService_test.py +85 -0
  64. naas_abi_core/services/cache/adapters/secondary/CacheFSAdapter.py +39 -0
  65. naas_abi_core/services/object_storage/ObjectStorageFactory.py +57 -0
  66. naas_abi_core/services/object_storage/ObjectStoragePort.py +47 -0
  67. naas_abi_core/services/object_storage/ObjectStorageService.py +41 -0
  68. naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterFS.py +52 -0
  69. naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterNaas.py +131 -0
  70. naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterS3.py +171 -0
  71. naas_abi_core/services/ontology/OntologyPorts.py +36 -0
  72. naas_abi_core/services/ontology/OntologyService.py +17 -0
  73. naas_abi_core/services/ontology/adaptors/secondary/OntologyService_SecondaryAdaptor_NERPort.py +37 -0
  74. naas_abi_core/services/secret/Secret.py +138 -0
  75. naas_abi_core/services/secret/SecretPorts.py +45 -0
  76. naas_abi_core/services/secret/Secret_test.py +65 -0
  77. naas_abi_core/services/secret/adaptors/secondary/Base64Secret.py +57 -0
  78. naas_abi_core/services/secret/adaptors/secondary/Base64Secret_test.py +39 -0
  79. naas_abi_core/services/secret/adaptors/secondary/NaasSecret.py +88 -0
  80. naas_abi_core/services/secret/adaptors/secondary/NaasSecret_test.py +25 -0
  81. naas_abi_core/services/secret/adaptors/secondary/dotenv_secret_secondaryadaptor.py +29 -0
  82. naas_abi_core/services/triple_store/TripleStoreFactory.py +116 -0
  83. naas_abi_core/services/triple_store/TripleStorePorts.py +223 -0
  84. naas_abi_core/services/triple_store/TripleStoreService.py +419 -0
  85. naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune.py +1300 -0
  86. naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune_test.py +284 -0
  87. naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph.py +597 -0
  88. naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph_test.py +1474 -0
  89. naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__Filesystem.py +223 -0
  90. naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__ObjectStorage.py +234 -0
  91. naas_abi_core/services/triple_store/adaptors/secondary/base/TripleStoreService__SecondaryAdaptor__FileBase.py +18 -0
  92. naas_abi_core/services/vector_store/IVectorStorePort.py +101 -0
  93. naas_abi_core/services/vector_store/IVectorStorePort_test.py +189 -0
  94. naas_abi_core/services/vector_store/VectorStoreFactory.py +47 -0
  95. naas_abi_core/services/vector_store/VectorStoreService.py +171 -0
  96. naas_abi_core/services/vector_store/VectorStoreService_test.py +185 -0
  97. naas_abi_core/services/vector_store/__init__.py +13 -0
  98. naas_abi_core/services/vector_store/adapters/QdrantAdapter.py +251 -0
  99. naas_abi_core/services/vector_store/adapters/QdrantAdapter_test.py +57 -0
  100. naas_abi_core/tests/test_services_imports.py +69 -0
  101. naas_abi_core/utils/Expose.py +55 -0
  102. naas_abi_core/utils/Graph.py +182 -0
  103. naas_abi_core/utils/JSON.py +49 -0
  104. naas_abi_core/utils/LazyLoader.py +44 -0
  105. naas_abi_core/utils/Logger.py +12 -0
  106. naas_abi_core/utils/OntologyReasoner.py +141 -0
  107. naas_abi_core/utils/OntologyYaml.py +681 -0
  108. naas_abi_core/utils/SPARQL.py +256 -0
  109. naas_abi_core/utils/Storage.py +33 -0
  110. naas_abi_core/utils/StorageUtils.py +398 -0
  111. naas_abi_core/utils/String.py +52 -0
  112. naas_abi_core/utils/Workers.py +114 -0
  113. naas_abi_core/utils/__init__.py +0 -0
  114. naas_abi_core/utils/onto2py/README.md +0 -0
  115. naas_abi_core/utils/onto2py/__init__.py +10 -0
  116. naas_abi_core/utils/onto2py/__main__.py +29 -0
  117. naas_abi_core/utils/onto2py/onto2py.py +611 -0
  118. naas_abi_core/utils/onto2py/tests/ttl2py_test.py +271 -0
  119. naas_abi_core/workflow/__init__.py +5 -0
  120. naas_abi_core/workflow/workflow.py +48 -0
  121. naas_abi_core-1.4.1.dist-info/METADATA +630 -0
  122. naas_abi_core-1.4.1.dist-info/RECORD +124 -0
  123. naas_abi_core-1.4.1.dist-info/WHEEL +4 -0
  124. 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
+ # ]