vectorwave 0.1.0__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.
@@ -0,0 +1,143 @@
1
+ import pytest
2
+ import json
3
+ from unittest.mock import patch, mock_open
4
+ from json import JSONDecodeError
5
+
6
+ # Function to test
7
+ from vectorwave.models.db_config import get_weaviate_settings
8
+
9
+ # --- Mock Data ---
10
+
11
+ # Mock content for a successfully loaded .weaviate_properties file
12
+ MOCK_JSON_DATA = """
13
+ {
14
+ "run_id": {
15
+ "data_type": "TEXT",
16
+ "description": "Test run ID"
17
+ },
18
+ "experiment_id": {
19
+ "data_type": "INT",
20
+ "description": "Identifier for the experiment"
21
+ }
22
+ }
23
+ """
24
+
25
+ # Mock content for a malformed .weaviate_properties file (invalid JSON)
26
+ MOCK_INVALID_JSON = """
27
+ {
28
+ "run_id": {
29
+ "data_type": "TEXT"
30
+ }
31
+ """ # Missing closing '}'
32
+
33
+ # --- Test Cases ---
34
+
35
+ @patch('os.path.exists', return_value=True)
36
+ @patch('builtins.open', new_callable=mock_open, read_data=MOCK_JSON_DATA)
37
+ def test_get_settings_loads_custom_props_success(mock_open_file, mock_exists):
38
+ """
39
+ Case 1: .weaviate_properties file exists and JSON is valid
40
+ - settings.custom_properties should be loaded correctly as a dictionary
41
+ """
42
+ # Arrange
43
+ # Clear the @lru_cache to bypass caching for this test
44
+ get_weaviate_settings.cache_clear()
45
+
46
+ # Act
47
+ settings = get_weaviate_settings()
48
+
49
+ # Assert
50
+ # Verify that the default path (.weaviate_properties) was checked
51
+ mock_exists.assert_called_with(".weaviate_properties")
52
+ # Verify the file was opened in 'r' mode
53
+ mock_open_file.assert_called_with(".weaviate_properties", 'r', encoding='utf-8')
54
+
55
+ assert settings.custom_properties is not None
56
+ assert "run_id" in settings.custom_properties
57
+ assert settings.custom_properties["run_id"]["data_type"] == "TEXT"
58
+ assert settings.custom_properties["run_id"]["description"] == "Test run ID"
59
+ assert "experiment_id" in settings.custom_properties
60
+
61
+
62
+ @patch('os.path.exists', return_value=False)
63
+ def test_get_settings_file_not_found(mock_exists, capsys):
64
+ """
65
+ Case 2: .weaviate_properties file does not exist
66
+ - settings.custom_properties should be None
67
+ - A 'file not found' note should be printed
68
+ """
69
+ # Arrange
70
+ get_weaviate_settings.cache_clear()
71
+
72
+ # Act
73
+ settings = get_weaviate_settings()
74
+
75
+ # Assert
76
+ mock_exists.assert_called_with(".weaviate_properties")
77
+ assert settings.custom_properties is None
78
+
79
+ # Check if 'file not found' note was printed
80
+ captured = capsys.readouterr()
81
+ assert "file not found" in captured.out
82
+
83
+
84
+ @patch('os.path.exists', return_value=True)
85
+ @patch('builtins.open', new_callable=mock_open, read_data=MOCK_INVALID_JSON)
86
+ @patch('json.load', side_effect=JSONDecodeError("Mock JSON Decode Error", "", 0))
87
+ def test_get_settings_invalid_json(mock_json_load, mock_open_file, mock_exists, capsys):
88
+ """
89
+ Case 3: File exists but JSON format is invalid
90
+ - settings.custom_properties should be None
91
+ - A 'Could not parse JSON' warning should be printed
92
+ """
93
+ # Arrange
94
+ get_weaviate_settings.cache_clear()
95
+
96
+ # Act
97
+ settings = get_weaviate_settings()
98
+
99
+ # Assert
100
+ mock_exists.assert_called_once()
101
+ mock_open_file.assert_called_once()
102
+ mock_json_load.assert_called_once() # json.load was called but failed (due to side_effect)
103
+ assert settings.custom_properties is None # Should be None due to parsing failure
104
+
105
+ # Check if 'Could not parse JSON' warning was printed
106
+ captured = capsys.readouterr()
107
+ assert "Could not parse JSON" in captured.out
108
+
109
+ @patch('os.path.exists', return_value=True)
110
+ @patch('builtins.open', new_callable=mock_open, read_data=MOCK_JSON_DATA)
111
+ @patch('os.environ.get') # os.environ.get을 모킹합니다
112
+ def test_get_settings_loads_global_custom_values(mock_env_get, mock_open_file, mock_exists):
113
+ """
114
+ Case 4: Test if the value of the "RUN_ID" environment variable is loaded
115
+ into global_custom_values for "run_id" defined in .weaviate_properties
116
+ """
117
+ # 1. Arrange
118
+ # MOCK_JSON_DATA defines "run_id" and "experiment_id".
119
+ # Set os.environ.get("RUN_ID") to return "test-run-123".
120
+ # Set os.environ.get("EXPERIMENT_ID") to return None.
121
+ def mock_env_side_effect(key):
122
+ if key == "RUN_ID":
123
+ return "test-run-123"
124
+ return None
125
+
126
+ mock_env_get.side_effect = mock_env_side_effect
127
+ get_weaviate_settings.cache_clear()
128
+
129
+ # 2. Act
130
+ settings = get_weaviate_settings()
131
+
132
+ # 3. Assert
133
+ # .weaviate_properties should be loaded correctly.
134
+ assert settings.custom_properties is not None
135
+ assert "run_id" in settings.custom_properties
136
+
137
+ # Check if global_custom_values was loaded correctly.
138
+ assert settings.global_custom_values is not None
139
+ assert "run_id" in settings.global_custom_values
140
+ assert settings.global_custom_values["run_id"] == "test-run-123"
141
+
142
+ # "EXPERIMENT_ID" should not be included as os.environ.get returned None.
143
+ assert "experiment_id" not in settings.global_custom_values
File without changes
File without changes
vectorwave/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .core.decorator import vectorize
2
+
3
+ from .database.db import initialize_database
4
+ from .database.db_search import search_functions, search_executions
5
+ from .monitoring.tracer import trace_span
6
+
7
+ __all__ = [
8
+ 'vectorize',
9
+ 'initialize_database',
10
+ 'search_functions',
11
+ 'search_executions',
12
+ 'trace_span'
13
+ ]
File without changes
@@ -0,0 +1,65 @@
1
+ import weaviate
2
+ import atexit
3
+ import logging
4
+ from functools import lru_cache
5
+ from ..models.db_config import get_weaviate_settings, WeaviateSettings
6
+ from ..database.db import get_weaviate_client
7
+ from ..exception.exceptions import WeaviateConnectionError
8
+
9
+
10
+
11
+ class WeaviateBatchManager:
12
+ """
13
+ A singleton class that manages Weaviate batch imports.
14
+ """
15
+
16
+ def __init__(self):
17
+ self._initialized = False
18
+ print("Initializing WeaviateBatchManager...")
19
+ self.client: weaviate.WeaviateClient = None
20
+
21
+ try:
22
+ # (get_weaviate_settings is reused as it is handled by lru_cache)
23
+ self.settings: WeaviateSettings = get_weaviate_settings()
24
+ self.client: weaviate.WeaviateClient = get_weaviate_client(self.settings)
25
+
26
+ if not self.client:
27
+ raise WeaviateConnectionError("Client is None, cannot configure batch.")
28
+
29
+ # self.client.batch.configure(
30
+ # batch_size=20,
31
+ # dynamic=True,
32
+ # timeout_retries=3,
33
+ # )
34
+
35
+ # Register atexit: Automatically calls self.flush() on script exit
36
+ # atexit.register(self.flush)
37
+ self._initialized = True
38
+ print("WeaviateBatchManager initialized and 'atexit' flush registered.")
39
+
40
+ except Exception as e:
41
+ # Prevents VectorWave from stopping the main app upon DB connection failure
42
+ print(f"Failed to initialize WeaviateBatchManager: {e}. Batching will be disabled.")
43
+
44
+ def add_object(self, collection: str, properties: dict, uuid: str = None):
45
+ """
46
+ Adds an object to the Weaviate batch queue.
47
+ """
48
+ if not self._initialized or not self.client:
49
+ print("Warning: Batch manager not initialized. Skipping add_object.") # print 유지
50
+ return
51
+
52
+ try:
53
+ self.client.collections.get(collection).data.insert(
54
+ properties=properties,
55
+ uuid=uuid
56
+ )
57
+
58
+ except Exception as e:
59
+ print(f"Error: Failed to add object to batch (Collection: {collection}): {e}")
60
+
61
+
62
+
63
+ @lru_cache(None)
64
+ def get_batch_manager() -> WeaviateBatchManager:
65
+ return WeaviateBatchManager()
File without changes
File without changes
@@ -0,0 +1,108 @@
1
+ # vtm/src/vectorwave/core/decorator.py
2
+
3
+ import inspect
4
+ from functools import wraps
5
+
6
+ from weaviate.util import generate_uuid5
7
+
8
+ from ..batch.batch import get_batch_manager
9
+ from ..models.db_config import get_weaviate_settings
10
+ from ..monitoring.tracer import trace_root, trace_span
11
+
12
+
13
+ def vectorize(search_description: str,
14
+ sequence_narrative: str,
15
+ **execution_tags):
16
+ """
17
+ VectorWave Decorator
18
+
19
+ (1) Collects function definitions (static data) once on script load.
20
+ (2) Records function execution (dynamic data) every time the function is called.
21
+ """
22
+
23
+ def decorator(func):
24
+
25
+ func_uuid = None
26
+ valid_execution_tags = {}
27
+ try:
28
+ module_name = func.__module__
29
+ function_name = func.__name__
30
+
31
+ func_identifier = f"{module_name}.{function_name}"
32
+ func_uuid = generate_uuid5(func_identifier)
33
+
34
+ static_properties = {
35
+ "function_name": function_name,
36
+ "module_name": module_name,
37
+ "docstring": inspect.getdoc(func) or "",
38
+ "source_code": inspect.getsource(func),
39
+ "search_description": search_description,
40
+ "sequence_narrative": sequence_narrative
41
+ }
42
+
43
+ batch = get_batch_manager()
44
+ settings = get_weaviate_settings()
45
+
46
+ if execution_tags:
47
+ if not settings.custom_properties:
48
+ print(
49
+ f"Warning: Function '{function_name}' provided execution_tags {list(execution_tags.keys())} "
50
+ f"but no .weaviate_properties file was loaded. These tags will be IGNORED."
51
+ )
52
+ else:
53
+ allowed_keys = set(settings.custom_properties.keys())
54
+ for key, value in execution_tags.items():
55
+ if key in allowed_keys:
56
+ valid_execution_tags[key] = value
57
+ else:
58
+ print(
59
+ f"Warning: Function '{function_name}' has undefined execution_tag: '{key}'. "
60
+ f"This tag will be IGNORED. Please add it to your .weaviate_properties file."
61
+ )
62
+
63
+ batch.add_object(
64
+ collection=settings.COLLECTION_NAME,
65
+ properties=static_properties,
66
+ uuid=func_uuid
67
+ )
68
+
69
+ except Exception as e:
70
+ print(f"Error in @vectorize setup for {func.__name__}: {e}")
71
+
72
+
73
+ # 2a. The *inner* wrapper to be wrapped by @trace_span
74
+ # This function receives all tags including full_kwargs from @trace_span.
75
+ @trace_root()
76
+ @trace_span(attributes_to_capture=['function_uuid', 'team', 'priority', 'run_id'])
77
+ @wraps(func)
78
+ def inner_wrapper(*args, **kwargs):
79
+
80
+ original_kwargs = kwargs.copy()
81
+
82
+ keys_to_remove = list(valid_execution_tags.keys())
83
+ keys_to_remove.append('function_uuid')
84
+
85
+ for key in execution_tags.keys():
86
+ if key not in keys_to_remove:
87
+ keys_to_remove.append(key)
88
+
89
+ for key in keys_to_remove:
90
+ original_kwargs.pop(key, None)
91
+
92
+ return func(*args, **original_kwargs)
93
+
94
+
95
+ @wraps(func)
96
+ def outer_wrapper(*args, **kwargs):
97
+
98
+ full_kwargs = kwargs.copy()
99
+ full_kwargs.update(valid_execution_tags)
100
+ full_kwargs['function_uuid'] = func_uuid
101
+
102
+ # 2. Call the *inner* wrapper with the full_kwargs
103
+ # This call passes through the @trace_root -> @trace_span decorators.
104
+ return inner_wrapper(*args, **full_kwargs)
105
+
106
+ return outer_wrapper
107
+
108
+ return decorator
File without changes
@@ -0,0 +1,302 @@
1
+ import weaviate
2
+ import weaviate.classes.config as wvc # (wvc = Weaviate Classes Config)
3
+ import weaviate.config as wvc_config
4
+ from weaviate.config import AdditionalConfig
5
+ from vectorwave.models.db_config import WeaviateSettings
6
+ from vectorwave.exception.exceptions import (
7
+ WeaviateConnectionError,
8
+ WeaviateNotReadyError,
9
+ SchemaCreationError
10
+ )
11
+ from functools import lru_cache
12
+ from weaviate.exceptions import WeaviateConnectionError as WeaviateClientConnectionError
13
+ from vectorwave.models.db_config import get_weaviate_settings
14
+
15
+
16
+ # Code based on Weaviate v4 (latest) client.
17
+
18
+ def get_weaviate_client(settings: WeaviateSettings) -> weaviate.WeaviateClient:
19
+ """
20
+ Creates and returns a Weaviate client.
21
+
22
+ [Raises]
23
+ - WeaviateConnectionError: If connection fails.
24
+ - WeaviateNotReadyError: If connected, but the server is not ready.
25
+ """
26
+
27
+ client: weaviate.WeaviateClient
28
+
29
+ try:
30
+ client = weaviate.connect_to_local(
31
+ host=settings.WEAVIATE_HOST,
32
+ port=settings.WEAVIATE_PORT,
33
+ grpc_port=settings.WEAVIATE_GRPC_PORT,
34
+ additional_config=AdditionalConfig(
35
+ dynamic=True,
36
+ batch_size=20,
37
+ timeout_retries=3
38
+ )
39
+ )
40
+ except WeaviateClientConnectionError as e:
41
+ raise WeaviateConnectionError(f"Failed to connect to Weaviate: {e}")
42
+ except Exception as e:
43
+ raise WeaviateConnectionError(f"An unknown error occurred while connecting to Weaviate: {e}")
44
+
45
+
46
+ if not client.is_ready():
47
+ raise WeaviateNotReadyError("Connected to Weaviate, but the server is not ready.")
48
+
49
+ print("Weaviate client connected successfully.")
50
+ return client
51
+
52
+ @lru_cache()
53
+ def get_cached_client() -> weaviate.WeaviateClient:
54
+ """
55
+ Singleton factory: Gets settings and returns a single client instance.
56
+ This function IS cached.
57
+ """
58
+ print("Creating and caching new Weaviate client instance...")
59
+ settings = get_weaviate_settings()
60
+ client = get_weaviate_client(settings)
61
+ return client
62
+
63
+
64
+ def create_vectorwave_schema(client: weaviate.WeaviateClient, settings: WeaviateSettings):
65
+ """
66
+ Defines and creates the VectorWaveFunctions collection schema.
67
+ Now includes custom properties loaded from the settings (via .weaviate_properties file).
68
+
69
+ [Raises]
70
+ - SchemaCreationError: If an error occurs during schema creation.
71
+ """
72
+ collection_name = settings.COLLECTION_NAME
73
+
74
+ # 1. Check if the collection already exists
75
+ if client.collections.exists(collection_name):
76
+ print(f"Collection '{collection_name}' already exists. Skipping creation.")
77
+ return client.collections.get(collection_name)
78
+
79
+ # 2. If it doesn't exist, define and create the collection
80
+ print(f"Creating collection '{collection_name}'...")
81
+
82
+ # 3. Define Base Properties
83
+ base_properties = [
84
+ wvc.Property(
85
+ name="function_name",
86
+ data_type=wvc.DataType.TEXT,
87
+ description="The name of the vectorized function"
88
+ ),
89
+ wvc.Property(
90
+ name="module_name",
91
+ data_type=wvc.DataType.TEXT,
92
+ description="The Python module path where the function is defined"
93
+ ),
94
+ wvc.Property(
95
+ name="docstring",
96
+ data_type=wvc.DataType.TEXT,
97
+ description="The function's Docstring (description)"
98
+ ),
99
+ wvc.Property(
100
+ name="source_code",
101
+ data_type=wvc.DataType.TEXT,
102
+ description="The actual source code of the function"
103
+ ),
104
+ wvc.Property(
105
+ name="search_description",
106
+ data_type=wvc.DataType.TEXT,
107
+ description="User-provided description for similarity search (from @vectorize)"
108
+ ),
109
+ wvc.Property(
110
+ name="sequence_narrative",
111
+ data_type=wvc.DataType.TEXT,
112
+ description="User-provided context about what happens next (from @vectorize)"
113
+ ),
114
+ ]
115
+
116
+ # 4. Parse Custom Properties (loaded from JSON file via settings object)
117
+ custom_properties = []
118
+ if settings.custom_properties:
119
+ print(f"Adding custom properties: {list(settings.custom_properties.keys())}")
120
+
121
+ for name, prop_details in settings.custom_properties.items():
122
+ if not isinstance(prop_details, dict):
123
+ raise SchemaCreationError(f"Custom property '{name}' in config file must be a dictionary.")
124
+
125
+ # Get data_type (Required)
126
+ dtype_str = prop_details.get("data_type")
127
+ if not dtype_str:
128
+ raise SchemaCreationError(f"Custom property '{name}' in config file is missing 'data_type'.")
129
+
130
+ # Get description (Optional)
131
+ description = prop_details.get("description")
132
+
133
+ try:
134
+ # Convert string (e.g., "TEXT") to Weaviate Enum (wvc.DataType.TEXT)
135
+ data_type = getattr(wvc.DataType, dtype_str.upper())
136
+
137
+ custom_properties.append(
138
+ wvc.Property(
139
+ name=name,
140
+ data_type=data_type,
141
+ description=description
142
+ )
143
+ )
144
+ except AttributeError:
145
+ raise SchemaCreationError(
146
+ f"Invalid data_type '{dtype_str}' for custom property '{name}'. "
147
+ f"Use a valid wvc.DataType string (e.g., 'TEXT', 'INT', 'NUMBER')."
148
+ )
149
+ except Exception as e:
150
+ raise SchemaCreationError(f"Error processing custom property '{name}': {e}")
151
+
152
+ # 5. Combine properties
153
+ all_properties = base_properties + custom_properties
154
+
155
+ vectorizer_name = settings.VECTORIZER_CONFIG.lower()
156
+ vector_config = None
157
+
158
+ print(f"Configuring vectorizer: {vectorizer_name}")
159
+
160
+ if vectorizer_name == "text2vec-openai":
161
+ vector_config = {
162
+ "vectorizer": "text2vec-openai",
163
+ "text2vec-openai": {
164
+ "vectorizeClassName": settings.IS_VECTORIZE_COLLECTION_NAME,
165
+ }
166
+ }
167
+ elif vectorizer_name == "none":
168
+ vector_config = {"vectorizer": "none"}
169
+ else:
170
+ raise SchemaCreationError(
171
+ f"Unsupported VECTORIZER_CONFIG: '{settings.VECTORIZER_CONFIG}'. "
172
+ f"Supported values: 'text2vec-openai', 'none'."
173
+ )
174
+
175
+ generative_config = None
176
+ if settings.GENERATIVE_CONFIG.lower() == "generative-openai":
177
+ generative_config = {"generator": "generative-openai"}
178
+
179
+
180
+ try:
181
+ vectorwave_collection = client.collections.create(
182
+ name=collection_name,
183
+ properties=all_properties,
184
+
185
+ # 7. Vectorizer Configuration
186
+ vector_config=vector_config,
187
+
188
+ # 8. Generative Configuration (for RAG, etc.)
189
+ generative_config=generative_config
190
+ )
191
+ return vectorwave_collection
192
+
193
+ except Exception as e:
194
+ # Raise a specific exception instead of returning None
195
+ raise SchemaCreationError(f"Error during schema creation: {e}")
196
+
197
+
198
+ def create_execution_schema(client: weaviate.WeaviateClient, settings: WeaviateSettings):
199
+ """
200
+ Defines and creates the VectorWaveExecutions (dynamic) collection schema.
201
+ """
202
+ collection_name = settings.EXECUTION_COLLECTION_NAME
203
+
204
+ if client.collections.exists(collection_name):
205
+ print(f"Collection '{collection_name}' already exists. Skipping creation.")
206
+ return client.collections.get(collection_name)
207
+
208
+ print(f"Creating collection '{collection_name}'...")
209
+
210
+ properties = [
211
+ wvc.Property(
212
+ name="trace_id",
213
+ data_type=wvc.DataType.TEXT,
214
+ description="The unique ID for the entire trace/workflow"
215
+ ),
216
+ wvc.Property(
217
+ name="span_id",
218
+ data_type=wvc.DataType.TEXT,
219
+ description="The unique ID for this specific span/function execution"
220
+ ),
221
+ wvc.Property(
222
+ name="function_name",
223
+ data_type=wvc.DataType.TEXT,
224
+ description="Name of the executed function (span name)"
225
+ ),
226
+ wvc.Property(
227
+ name="function_uuid",
228
+ data_type=wvc.DataType.UUID,
229
+ description="The UUID of the executed function definition"
230
+ ),
231
+ wvc.Property(
232
+ name="timestamp_utc",
233
+ data_type=wvc.DataType.DATE,
234
+ description="The UTC timestamp when the execution started"
235
+ ),
236
+ wvc.Property(
237
+ name="duration_ms",
238
+ data_type=wvc.DataType.NUMBER,
239
+ description="Total execution time in milliseconds"
240
+ ),
241
+ wvc.Property(
242
+ name="status",
243
+ data_type=wvc.DataType.TEXT, # "SUCCESS" or "ERROR"
244
+ description="Execution status"
245
+ ),
246
+ wvc.Property(
247
+ name="error_message",
248
+ data_type=wvc.DataType.TEXT,
249
+ description="Error message and traceback if status is 'ERROR'"
250
+ ),
251
+ ]
252
+
253
+ if settings.custom_properties:
254
+ print(f"Adding custom properties to '{collection_name}': {list(settings.custom_properties.keys())}")
255
+ for name, prop_details in settings.custom_properties.items():
256
+ try:
257
+ if not isinstance(prop_details, dict):
258
+ raise ValueError("Property details must be a dictionary.")
259
+
260
+ dtype_str = prop_details.get("data_type")
261
+ if not dtype_str:
262
+ raise ValueError("data_type is missing.")
263
+
264
+ data_type = getattr(wvc.DataType, dtype_str.upper())
265
+ description = prop_details.get("description")
266
+
267
+ properties.append(
268
+ wvc.Property(
269
+ name=name,
270
+ data_type=data_type,
271
+ description=description
272
+ )
273
+ )
274
+ except Exception as e:
275
+ print(f"Warning: Skipping custom property {name} for {collection_name}: {e}")
276
+
277
+ try:
278
+ execution_collection = client.collections.create(
279
+ name=collection_name,
280
+ properties=properties,
281
+ vectorizer_config=wvc.Configure.Vectorizer.none(),
282
+ vector_index_config=wvc.Configure.VectorIndex.none()
283
+ )
284
+ print(f"Collection '{collection_name}' created successfully.")
285
+ return execution_collection
286
+ except Exception as e:
287
+ raise SchemaCreationError(f"Error during execution schema creation: {e}")
288
+
289
+ def initialize_database():
290
+ """
291
+ Helper function to initialize both the client and the two schemas.
292
+ """
293
+ try:
294
+ settings = get_weaviate_settings()
295
+ client = get_cached_client()
296
+ if client:
297
+ create_vectorwave_schema(client, settings)
298
+ create_execution_schema(client, settings)
299
+ return client
300
+ except Exception as e:
301
+ print(f"Failed to initialize VectorWave database: {e}")
302
+ return None