vectorwave 0.1.9__tar.gz → 0.2.0__tar.gz
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.
- {vectorwave-0.1.9/src/vectorwave.egg-info → vectorwave-0.2.0}/PKG-INFO +1 -1
- {vectorwave-0.1.9 → vectorwave-0.2.0}/pyproject.toml +1 -1
- vectorwave-0.2.0/src/tests/utils/test_return_caching.py +126 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/__init__.py +7 -1
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/database/db_search.py +52 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/utils/replayer.py +38 -9
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/utils/replayer_semantic.py +43 -35
- vectorwave-0.2.0/src/vectorwave/utils/return_caching_utils.py +134 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0/src/vectorwave.egg-info}/PKG-INFO +1 -1
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave.egg-info/SOURCES.txt +1 -0
- vectorwave-0.1.9/src/vectorwave/utils/return_caching_utils.py +0 -76
- {vectorwave-0.1.9 → vectorwave-0.2.0}/LICENSE +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/MANIFEST.in +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/NOTICE +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/Readme.md +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/setup.cfg +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/batch/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/batch/test_batch.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/conftest.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/core/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/core/test_decorator.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/core/test_semantic_caching.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/database/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/database/test_archiver.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/database/test_db.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/database/test_db_search.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/exception/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/models/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/models/test_db_config.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/monitoring/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/monitoring/alert/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/monitoring/alert/test_alerter.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/monitoring/test_async_trace.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/monitoring/test_tracer.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/prediction/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/search/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/search/test_execution_search.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/utils/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/utils/test_function_cahe.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/utils/test_replayer.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/utils/test_status.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/tests/vectorizer/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/batch/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/batch/batch.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/core/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/core/core.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/core/decorator.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/core/generator.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/database/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/database/archiver.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/database/db.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/exception/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/exception/exceptions.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/models/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/models/db_config.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/monitoring/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/monitoring/alert/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/monitoring/alert/base.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/monitoring/alert/factory.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/monitoring/alert/null_alerter.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/monitoring/alert/webhook_alerter.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/monitoring/monitoring.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/monitoring/tracer.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/prediction/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/prediction/predictor.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/search/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/search/execution_search.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/search/extended_search.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/search/rag_search.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/utils/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/utils/function_cache.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/utils/healer.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/utils/status.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/vectorizer/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/vectorizer/base.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/vectorizer/factory.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/vectorizer/huggingface_vectorizer.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave/vectorizer/openai_vectorizer.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave.egg-info/dependency_links.txt +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave.egg-info/requires.txt +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.0}/src/vectorwave.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
import json
|
|
4
|
+
from vectorwave.utils.return_caching_utils import _check_and_return_cached_result
|
|
5
|
+
from vectorwave.models.db_config import WeaviateSettings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# --- Mock Fixtures ---
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_caching_utils_deps(monkeypatch):
|
|
12
|
+
"""
|
|
13
|
+
Mocks external dependencies of return_caching_utils.py (BatchManager, Tracer, DB search, etc.).
|
|
14
|
+
"""
|
|
15
|
+
# 1. Mock Settings
|
|
16
|
+
mock_settings = WeaviateSettings(
|
|
17
|
+
EXECUTION_COLLECTION_NAME="TestExecutions",
|
|
18
|
+
global_custom_values={"run_id": "test-run-123"}
|
|
19
|
+
)
|
|
20
|
+
mock_get_settings = MagicMock(return_value=mock_settings)
|
|
21
|
+
|
|
22
|
+
# 2. Mock Batch Manager (Key verification target)
|
|
23
|
+
mock_batch_manager = MagicMock()
|
|
24
|
+
mock_batch_manager.add_object = MagicMock()
|
|
25
|
+
mock_get_batch = MagicMock(return_value=mock_batch_manager)
|
|
26
|
+
|
|
27
|
+
# 3. Mock Vectorizer
|
|
28
|
+
mock_vectorizer = MagicMock()
|
|
29
|
+
mock_vectorizer.embed.return_value = [0.1, 0.2, 0.3] # Dummy Vector
|
|
30
|
+
mock_get_vectorizer = MagicMock(return_value=mock_vectorizer)
|
|
31
|
+
|
|
32
|
+
# 4. Mock Tracer Context (Provides current Trace ID)
|
|
33
|
+
mock_tracer = MagicMock()
|
|
34
|
+
mock_tracer.trace_id = "existing-trace-id-abc"
|
|
35
|
+
|
|
36
|
+
# 5. Apply Monkeypatches
|
|
37
|
+
TARGET_MODULE = "vectorwave.utils.return_caching_utils"
|
|
38
|
+
|
|
39
|
+
monkeypatch.setattr(f"{TARGET_MODULE}.get_weaviate_settings", mock_get_settings)
|
|
40
|
+
monkeypatch.setattr(f"{TARGET_MODULE}.get_batch_manager", mock_get_batch)
|
|
41
|
+
monkeypatch.setattr(f"{TARGET_MODULE}.get_vectorizer", mock_get_vectorizer)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
"batch_manager": mock_batch_manager,
|
|
45
|
+
"vectorizer": mock_vectorizer,
|
|
46
|
+
"tracer_obj": mock_tracer
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_check_and_return_cached_result_cache_hit_logging(mock_caching_utils_deps):
|
|
51
|
+
"""
|
|
52
|
+
[Case 1] Verify that DB logging is correctly performed with 'CACHE_HIT' status upon a cache hit.
|
|
53
|
+
"""
|
|
54
|
+
# Arrange
|
|
55
|
+
# 1. Mock cache search result (Log found in DB)
|
|
56
|
+
mock_cached_log = {
|
|
57
|
+
"return_value": json.dumps({"result": "cached_data"}),
|
|
58
|
+
"metadata": {"distance": 0.1},
|
|
59
|
+
"uuid": "cached-log-uuid"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# 2. Mock search_similar_execution to return the above result (Simulate Cache Hit)
|
|
63
|
+
with patch("vectorwave.utils.return_caching_utils.search_similar_execution", return_value=mock_cached_log):
|
|
64
|
+
# [Fix] Instead of directly patching the .get method of the ContextVar object, replace the variable itself with a Mock object.
|
|
65
|
+
with patch("vectorwave.utils.return_caching_utils.current_tracer_var") as mock_tracer_var:
|
|
66
|
+
with patch("vectorwave.utils.return_caching_utils.current_span_id_var") as mock_span_var:
|
|
67
|
+
# Set return value for .get() call on the Mock object
|
|
68
|
+
mock_tracer_var.get.return_value = mock_caching_utils_deps["tracer_obj"]
|
|
69
|
+
mock_span_var.get.return_value = "parent-span-123"
|
|
70
|
+
|
|
71
|
+
# 4. Execute the target function
|
|
72
|
+
def dummy_func(a, b): pass # Target function
|
|
73
|
+
|
|
74
|
+
result = _check_and_return_cached_result(
|
|
75
|
+
func=dummy_func,
|
|
76
|
+
args=(10,),
|
|
77
|
+
kwargs={"b": 20},
|
|
78
|
+
function_name="dummy_func",
|
|
79
|
+
cache_threshold=0.9,
|
|
80
|
+
is_async=False
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Assert
|
|
84
|
+
# 1. Was the cached result returned correctly?
|
|
85
|
+
assert result == {"result": "cached_data"}
|
|
86
|
+
|
|
87
|
+
# 2. [Core] Was BatchManager.add_object called? (Check if logging occurred)
|
|
88
|
+
mock_batch = mock_caching_utils_deps["batch_manager"]
|
|
89
|
+
mock_batch.add_object.assert_called_once()
|
|
90
|
+
|
|
91
|
+
# 3. [Core] Verify properties of the saved log
|
|
92
|
+
call_kwargs = mock_batch.add_object.call_args.kwargs
|
|
93
|
+
props = call_kwargs["properties"]
|
|
94
|
+
|
|
95
|
+
assert props["status"] == "CACHE_HIT" # Check if status is CACHE_HIT
|
|
96
|
+
assert props["duration_ms"] == 0.0 # Check if duration is 0
|
|
97
|
+
assert props["trace_id"] == "existing-trace-id-abc" # Check if existing Trace ID is maintained
|
|
98
|
+
assert props["parent_span_id"] == "parent-span-123" # Check for parent Span ID maintenance
|
|
99
|
+
assert props["function_name"] == "dummy_func"
|
|
100
|
+
assert props["run_id"] == "test-run-123" # Check for inclusion of global tags
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_check_and_return_cached_result_cache_miss(mock_caching_utils_deps):
|
|
104
|
+
"""
|
|
105
|
+
[Case 2] Verify that None is returned without logging upon a cache miss.
|
|
106
|
+
"""
|
|
107
|
+
# Arrange
|
|
108
|
+
# Set search_similar_execution to return None (Cache Miss)
|
|
109
|
+
with patch("vectorwave.utils.return_caching_utils.search_similar_execution", return_value=None):
|
|
110
|
+
def dummy_func(): pass
|
|
111
|
+
|
|
112
|
+
result = _check_and_return_cached_result(
|
|
113
|
+
func=dummy_func,
|
|
114
|
+
args=(),
|
|
115
|
+
kwargs={},
|
|
116
|
+
function_name="dummy_func",
|
|
117
|
+
cache_threshold=0.9,
|
|
118
|
+
is_async=False
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Assert
|
|
122
|
+
# 1. Result should be None (To proceed to actual function execution)
|
|
123
|
+
assert result is None
|
|
124
|
+
|
|
125
|
+
# 2. BatchManager.add_object should not be called
|
|
126
|
+
mock_caching_utils_deps["batch_manager"].add_object.assert_not_called()
|
|
@@ -5,6 +5,9 @@ from .database.db_search import search_functions, search_executions, search_erro
|
|
|
5
5
|
from .monitoring.tracer import trace_span
|
|
6
6
|
from .search.rag_search import search_and_answer, analyze_trace_log
|
|
7
7
|
from .core.generator import generate_and_register_metadata
|
|
8
|
+
from .utils.healer import VectorWaveHealer
|
|
9
|
+
from .utils.replayer import VectorWaveReplayer
|
|
10
|
+
from .utils.replayer_semantic import SemanticReplayer
|
|
8
11
|
|
|
9
12
|
__all__ = [
|
|
10
13
|
'vectorize',
|
|
@@ -16,5 +19,8 @@ __all__ = [
|
|
|
16
19
|
'trace_span',
|
|
17
20
|
'search_and_answer',
|
|
18
21
|
'analyze_trace_log',
|
|
19
|
-
'generate_and_register_metadata'
|
|
22
|
+
'generate_and_register_metadata',
|
|
23
|
+
'VectorWaveHealer',
|
|
24
|
+
'VectorWaveReplayer',
|
|
25
|
+
'SemanticReplayer'
|
|
20
26
|
]
|
|
@@ -409,3 +409,55 @@ def check_semantic_drift(
|
|
|
409
409
|
except Exception as e:
|
|
410
410
|
logger.error(f"Failed to check semantic drift: {e}")
|
|
411
411
|
return False, 0.0, None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def simulate_drift_check(
|
|
415
|
+
text: str,
|
|
416
|
+
function_name: str,
|
|
417
|
+
threshold: Optional[float] = None,
|
|
418
|
+
k: Optional[int] = None
|
|
419
|
+
) -> Dict[str, Any]:
|
|
420
|
+
"""
|
|
421
|
+
Simulates drift detection for a hypothetical input string without executing the function.
|
|
422
|
+
Useful for 'Drift Radar' or debugging.
|
|
423
|
+
"""
|
|
424
|
+
try:
|
|
425
|
+
settings = get_weaviate_settings()
|
|
426
|
+
vectorizer = get_vectorizer()
|
|
427
|
+
|
|
428
|
+
if not vectorizer:
|
|
429
|
+
return {"error": "No vectorizer configured."}
|
|
430
|
+
|
|
431
|
+
# 1. Set defaults from settings if not provided
|
|
432
|
+
if threshold is None:
|
|
433
|
+
threshold = settings.DRIFT_DISTANCE_THRESHOLD
|
|
434
|
+
if k is None:
|
|
435
|
+
k = settings.DRIFT_NEIGHBOR_AMOUNT
|
|
436
|
+
|
|
437
|
+
# 2. Vectorize the input text
|
|
438
|
+
try:
|
|
439
|
+
vector = vectorizer.embed(text)
|
|
440
|
+
except Exception as e:
|
|
441
|
+
return {"error": f"Vectorization failed: {e}"}
|
|
442
|
+
|
|
443
|
+
# 3. Perform the check using the existing logic
|
|
444
|
+
is_drift, avg_distance, nearest_uuid = check_semantic_drift(
|
|
445
|
+
vector=vector,
|
|
446
|
+
function_name=function_name,
|
|
447
|
+
threshold=threshold,
|
|
448
|
+
k=k
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
"function_name": function_name,
|
|
453
|
+
"input_text": text,
|
|
454
|
+
"is_drift": is_drift,
|
|
455
|
+
"avg_distance": avg_distance,
|
|
456
|
+
"threshold": threshold,
|
|
457
|
+
"nearest_neighbor_uuid": nearest_uuid,
|
|
458
|
+
"status": "ANOMALY" if is_drift else "NORMAL"
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
except Exception as e:
|
|
462
|
+
logger.error(f"Simulation failed: {e}")
|
|
463
|
+
return {"error": str(e)}
|
|
@@ -5,6 +5,8 @@ import traceback
|
|
|
5
5
|
import inspect
|
|
6
6
|
from typing import Any, Dict, List, Optional
|
|
7
7
|
import asyncio
|
|
8
|
+
import difflib
|
|
9
|
+
import pprint
|
|
8
10
|
|
|
9
11
|
import weaviate.classes.query as wvc_query
|
|
10
12
|
|
|
@@ -33,6 +35,7 @@ class VectorWaveReplayer:
|
|
|
33
35
|
update_baseline: bool = False) -> Dict[str, Any]:
|
|
34
36
|
"""
|
|
35
37
|
Retrieves past execution history of a specific function, re-executes it (Replay), and validates the result.
|
|
38
|
+
Returns a dictionary containing diff_html for UI visualization.
|
|
36
39
|
"""
|
|
37
40
|
# 1. Dynamic Function Loading (Dynamic Import)
|
|
38
41
|
try:
|
|
@@ -72,7 +75,7 @@ class VectorWaveReplayer:
|
|
|
72
75
|
logger.warning(f"No data found to test: {function_full_name}")
|
|
73
76
|
return results
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
logger.info(f"Starting Replay: {len(response.objects)} logs for '{function_full_name}'")
|
|
76
79
|
|
|
77
80
|
for obj in response.objects:
|
|
78
81
|
results["total"] += 1
|
|
@@ -92,37 +95,46 @@ class VectorWaveReplayer:
|
|
|
92
95
|
|
|
93
96
|
if is_match:
|
|
94
97
|
results["passed"] += 1
|
|
98
|
+
logger.debug(f"UUID {obj.uuid}: PASSED")
|
|
95
99
|
else:
|
|
96
100
|
# 5. Handle Mismatch
|
|
97
101
|
if update_baseline:
|
|
98
102
|
self._update_baseline_value(collection, obj.uuid, actual_output)
|
|
99
103
|
results["updated"] += 1
|
|
100
104
|
results["passed"] += 1
|
|
101
|
-
|
|
105
|
+
logger.info(f"UUID {obj.uuid}: Baseline UPDATED")
|
|
102
106
|
else:
|
|
103
107
|
results["failed"] += 1
|
|
108
|
+
|
|
109
|
+
# Generate Visual Diff
|
|
110
|
+
diff_html = self._generate_diff_html(expected_output, actual_output)
|
|
111
|
+
|
|
104
112
|
results["failures"].append({
|
|
105
113
|
"uuid": str(obj.uuid),
|
|
106
114
|
"inputs": inputs,
|
|
107
115
|
"expected": expected_output,
|
|
108
|
-
"actual": actual_output
|
|
116
|
+
"actual": actual_output,
|
|
117
|
+
"diff_html": diff_html # UI visualization field
|
|
109
118
|
})
|
|
110
|
-
|
|
119
|
+
logger.warning(f"UUID {obj.uuid}: FAILED (Mismatch)")
|
|
111
120
|
|
|
112
121
|
except Exception as e:
|
|
113
|
-
|
|
114
122
|
results["failed"] += 1
|
|
123
|
+
error_msg = f"Exception: {str(e)}"
|
|
124
|
+
logger.error(f"UUID {obj.uuid}: EXECUTION ERROR - {e}")
|
|
115
125
|
|
|
116
126
|
results["failures"].append({
|
|
117
127
|
"uuid": str(obj.uuid),
|
|
118
128
|
"inputs": inputs,
|
|
119
|
-
"expected": expected_output,
|
|
120
|
-
"actual": "EXCEPTION_RAISED",
|
|
121
|
-
"error":
|
|
129
|
+
"expected": expected_output,
|
|
130
|
+
"actual": "EXCEPTION_RAISED",
|
|
131
|
+
"error": error_msg,
|
|
132
|
+
"diff_html": f"<div class='error' style='color:red; padding:10px; border:1px solid red; background:#fff0f0;"
|
|
133
|
+
f"'><strong>Runtime Error:</strong><pre>{traceback.format_exc()}</pre></div>",
|
|
122
134
|
"traceback": traceback.format_exc()
|
|
123
135
|
})
|
|
124
|
-
print(f" ❌ [Error] UUID {obj.uuid} exception occurred during execution: {e}")
|
|
125
136
|
|
|
137
|
+
logger.info(f"Replay Finished. Passed: {results['passed']}, Failed: {results['failed']}, Updated: {results['updated']}")
|
|
126
138
|
return results
|
|
127
139
|
|
|
128
140
|
def _extract_inputs(self, props: Dict[str, Any], target_func: callable) -> Dict[str, Any]:
|
|
@@ -180,3 +192,20 @@ class VectorWaveReplayer:
|
|
|
180
192
|
)
|
|
181
193
|
except Exception as e:
|
|
182
194
|
logger.error(f"Failed to update baseline for {uuid}: {e}")
|
|
195
|
+
|
|
196
|
+
def _generate_diff_html(self, expected: Any, actual: Any) -> str:
|
|
197
|
+
"""Generates a side-by-side HTML diff table."""
|
|
198
|
+
# Pretty print for better diff comparison
|
|
199
|
+
exp_str = pprint.pformat(expected, width=80)
|
|
200
|
+
act_str = pprint.pformat(actual, width=80)
|
|
201
|
+
|
|
202
|
+
# Generate HTML table
|
|
203
|
+
diff_html = difflib.HtmlDiff(wrapcolumn=80).make_table(
|
|
204
|
+
fromlines=exp_str.splitlines(),
|
|
205
|
+
tolines=act_str.splitlines(),
|
|
206
|
+
fromdesc='Expected (Baseline)',
|
|
207
|
+
todesc='Actual (Current)',
|
|
208
|
+
context=True, # Show only context around changes
|
|
209
|
+
numlines=3
|
|
210
|
+
)
|
|
211
|
+
return diff_html
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import importlib
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
import traceback
|
|
4
5
|
import math
|
|
5
6
|
import asyncio
|
|
6
7
|
import inspect
|
|
7
|
-
from typing import Any, Dict, Optional
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
8
9
|
|
|
9
10
|
import weaviate.classes.query as wvc_query
|
|
10
11
|
|
|
@@ -23,6 +24,7 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
23
24
|
"""
|
|
24
25
|
A subclass of VectorWaveReplayer that adds AI-powered semantic comparison capabilities.
|
|
25
26
|
It supports Vector Cosine Similarity and LLM-as-a-Judge evaluation.
|
|
27
|
+
Location: src/vectorwave/utils/replayer_semantic.py
|
|
26
28
|
"""
|
|
27
29
|
|
|
28
30
|
def __init__(self):
|
|
@@ -39,22 +41,28 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
39
41
|
function_full_name: str,
|
|
40
42
|
limit: int = 10,
|
|
41
43
|
update_baseline: bool = False,
|
|
42
|
-
similarity_threshold: Optional[float] = None,
|
|
43
|
-
semantic_eval: bool = False
|
|
44
|
+
similarity_threshold: Optional[float] = None,
|
|
45
|
+
semantic_eval: bool = False
|
|
44
46
|
) -> Dict[str, Any]:
|
|
45
47
|
"""
|
|
46
|
-
|
|
48
|
+
Retrieves past execution history of a specific function, re-executes it (Replay),
|
|
49
|
+
and validates the result using semantic comparison options.
|
|
50
|
+
Returns a dictionary containing diff_html for UI visualization.
|
|
47
51
|
"""
|
|
48
|
-
# 1.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
# 1. Dynamic Function Loading (Dynamic Import)
|
|
53
|
+
try:
|
|
54
|
+
module_name, func_short_name = function_full_name.rsplit('.', 1)
|
|
55
|
+
module = importlib.import_module(module_name)
|
|
56
|
+
target_func = getattr(module, func_short_name)
|
|
57
|
+
except (ValueError, ImportError, AttributeError) as e:
|
|
58
|
+
logger.error(f"Could not load function: {function_full_name}. Error: {e}")
|
|
59
|
+
return {"error": f"Function loading failed: {e}"}
|
|
52
60
|
|
|
53
61
|
is_async_func = inspect.iscoroutinefunction(target_func)
|
|
54
|
-
func_short_name = function_full_name.rsplit('.', 1)[-1]
|
|
55
62
|
|
|
56
|
-
# 2. Retrieve Data
|
|
63
|
+
# 2. Retrieve Test Data (Past Logs) from DB
|
|
57
64
|
collection = self.client.collections.get(self.collection_name)
|
|
65
|
+
|
|
58
66
|
filters = (
|
|
59
67
|
wvc_query.Filter.by_property("function_name").equal(func_short_name) &
|
|
60
68
|
wvc_query.Filter.by_property("status").equal("SUCCESS")
|
|
@@ -79,25 +87,26 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
79
87
|
logger.warning(f"No data found to test: {function_full_name}")
|
|
80
88
|
return results
|
|
81
89
|
|
|
82
|
-
|
|
90
|
+
logger.info(f"Starting Semantic Replay: {len(response.objects)} logs for '{function_full_name}'")
|
|
83
91
|
if similarity_threshold:
|
|
84
|
-
|
|
92
|
+
logger.info(f" - Mode: Vector Similarity >= {similarity_threshold}")
|
|
85
93
|
if semantic_eval:
|
|
86
|
-
|
|
94
|
+
logger.info(f" - Mode: Semantic Evaluation (LLM-as-a-Judge)")
|
|
87
95
|
|
|
88
96
|
for obj in response.objects:
|
|
89
97
|
results["total"] += 1
|
|
98
|
+
|
|
90
99
|
inputs = self._extract_inputs(obj.properties, target_func)
|
|
91
100
|
expected_output = self._deserialize_value(obj.properties.get("return_value"))
|
|
92
101
|
|
|
93
102
|
try:
|
|
94
|
-
# 3. Re-execution
|
|
103
|
+
# 3. Function Re-execution
|
|
95
104
|
if is_async_func:
|
|
96
105
|
actual_output = asyncio.run(target_func(**inputs))
|
|
97
106
|
else:
|
|
98
107
|
actual_output = target_func(**inputs)
|
|
99
108
|
|
|
100
|
-
# 4.
|
|
109
|
+
# 4. Result Validation (Semantic Comparison)
|
|
101
110
|
is_match, match_reason = self._compare_results_semantic(
|
|
102
111
|
expected_output,
|
|
103
112
|
actual_output,
|
|
@@ -107,50 +116,49 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
107
116
|
|
|
108
117
|
if is_match:
|
|
109
118
|
results["passed"] += 1
|
|
119
|
+
logger.debug(f"UUID {obj.uuid}: PASSED ({match_reason})")
|
|
110
120
|
else:
|
|
111
121
|
# 5. Handle Mismatch
|
|
112
122
|
if update_baseline:
|
|
113
123
|
self._update_baseline_value(collection, obj.uuid, actual_output)
|
|
114
124
|
results["updated"] += 1
|
|
115
125
|
results["passed"] += 1
|
|
116
|
-
|
|
126
|
+
logger.info(f"UUID {obj.uuid}: Baseline UPDATED")
|
|
117
127
|
else:
|
|
118
128
|
results["failed"] += 1
|
|
129
|
+
|
|
130
|
+
# Generate Visual Diff (inherited from parent)
|
|
131
|
+
diff_html = self._generate_diff_html(expected_output, actual_output)
|
|
132
|
+
|
|
119
133
|
results["failures"].append({
|
|
120
134
|
"uuid": str(obj.uuid),
|
|
121
135
|
"inputs": inputs,
|
|
122
136
|
"expected": expected_output,
|
|
123
137
|
"actual": actual_output,
|
|
124
|
-
"
|
|
138
|
+
"diff_html": diff_html, # UI visualization field
|
|
139
|
+
"reason": match_reason # Semantic failure reason
|
|
125
140
|
})
|
|
126
|
-
|
|
127
|
-
print(f" Expected: {str(expected_output)[:60]}...")
|
|
128
|
-
print(f" Actual: {str(actual_output)[:60]}...")
|
|
141
|
+
logger.warning(f"UUID {obj.uuid}: FAILED ({match_reason})")
|
|
129
142
|
|
|
130
143
|
except Exception as e:
|
|
131
144
|
results["failed"] += 1
|
|
145
|
+
error_msg = f"Exception: {str(e)}"
|
|
146
|
+
logger.error(f"UUID {obj.uuid}: EXECUTION ERROR - {e}")
|
|
147
|
+
|
|
132
148
|
results["failures"].append({
|
|
133
149
|
"uuid": str(obj.uuid),
|
|
134
150
|
"inputs": inputs,
|
|
135
151
|
"expected": expected_output,
|
|
136
152
|
"actual": "EXCEPTION_RAISED",
|
|
137
|
-
"error":
|
|
153
|
+
"error": error_msg,
|
|
154
|
+
"diff_html": f"<div class='error' style='color:red; padding:10px; border:1px solid red; background:#fff0f0;"
|
|
155
|
+
f"'><strong>Runtime Error:</strong><pre>{traceback.format_exc()}</pre></div>",
|
|
138
156
|
"traceback": traceback.format_exc()
|
|
139
157
|
})
|
|
140
|
-
print(f" ❌ [Error] UUID {obj.uuid}: {e}")
|
|
141
158
|
|
|
159
|
+
logger.info(f"Replay Finished. Passed: {results['passed']}, Failed: {results['failed']}, Updated: {results['updated']}")
|
|
142
160
|
return results
|
|
143
161
|
|
|
144
|
-
def _load_target_function(self, full_name: str):
|
|
145
|
-
"""Helper to load function (Refactored from original code for reuse)"""
|
|
146
|
-
try:
|
|
147
|
-
import importlib
|
|
148
|
-
module_name, func_short_name = full_name.rsplit('.', 1)
|
|
149
|
-
module = importlib.import_module(module_name)
|
|
150
|
-
return getattr(module, func_short_name)
|
|
151
|
-
except Exception as e:
|
|
152
|
-
return {"error": str(e)}
|
|
153
|
-
|
|
154
162
|
def _compare_results_semantic(self, expected: Any, actual: Any,
|
|
155
163
|
similarity_threshold: Optional[float],
|
|
156
164
|
semantic_eval: bool) -> tuple[bool, str]:
|
|
@@ -205,10 +213,10 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
205
213
|
prompt = f"""
|
|
206
214
|
Compare two outputs. Are they semantically equivalent?
|
|
207
215
|
Ignore minor formatting differences.
|
|
208
|
-
|
|
216
|
+
|
|
209
217
|
Expected: {expected}
|
|
210
218
|
Actual: {actual}
|
|
211
|
-
|
|
219
|
+
|
|
212
220
|
Respond JSON: {{"equivalent": true}} or {{"equivalent": false}}
|
|
213
221
|
"""
|
|
214
222
|
try:
|
|
@@ -221,4 +229,4 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
221
229
|
return json.loads(res.choices[0].message.content).get("equivalent", False)
|
|
222
230
|
except Exception as e:
|
|
223
231
|
logger.error(f"LLM Eval failed: {e}")
|
|
224
|
-
return False
|
|
232
|
+
return False
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional, Tuple, Any, Dict, Callable
|
|
3
|
+
import json
|
|
4
|
+
import inspect
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from weaviate.util import generate_uuid5
|
|
10
|
+
|
|
11
|
+
from ..models.db_config import get_weaviate_settings, WeaviateSettings
|
|
12
|
+
from ..monitoring.tracer import _create_input_vector_data, _deserialize_return_value, current_tracer_var, \
|
|
13
|
+
current_span_id_var
|
|
14
|
+
from ..database.db_search import search_similar_execution
|
|
15
|
+
from ..vectorizer.factory import get_vectorizer
|
|
16
|
+
from ..batch.batch import get_batch_manager
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _check_and_return_cached_result(
|
|
22
|
+
func: Callable,
|
|
23
|
+
args: Tuple[Any, ...],
|
|
24
|
+
kwargs: Dict[str, Any],
|
|
25
|
+
function_name: str,
|
|
26
|
+
cache_threshold: float,
|
|
27
|
+
is_async: bool
|
|
28
|
+
) -> Optional[Any]:
|
|
29
|
+
"""
|
|
30
|
+
Checks the VectorWaveExecutions database for a semantically similar cached result.
|
|
31
|
+
If found, returns the deserialized result; otherwise, returns None.
|
|
32
|
+
|
|
33
|
+
This function should be called from the outer_wrapper of the @vectorize decorator.
|
|
34
|
+
"""
|
|
35
|
+
if not cache_threshold: # Check if caching is effectively disabled (threshold 0 or None)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
settings: WeaviateSettings = get_weaviate_settings()
|
|
39
|
+
vectorizer = get_vectorizer()
|
|
40
|
+
|
|
41
|
+
if vectorizer is None:
|
|
42
|
+
# This case should ideally be caught in decorator setup, but defensive check remains.
|
|
43
|
+
logger.error(f"Cannot perform vectorization for caching on '{function_name}': Vectorizer is None.")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# (A) Create vectorization data
|
|
48
|
+
input_vector_data = _create_input_vector_data(
|
|
49
|
+
func_name=function_name,
|
|
50
|
+
args=args, # Original positional args
|
|
51
|
+
kwargs=kwargs, # Original keyword args
|
|
52
|
+
sensitive_keys=settings.sensitive_keys
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# (B) Vectorize (Synchronous call is fine, as vectorizer.embed is sync)
|
|
56
|
+
input_vector = vectorizer.embed(input_vector_data['text'])
|
|
57
|
+
|
|
58
|
+
# (C) Search Cache (Needs to handle the fact that search_similar_execution is sync)
|
|
59
|
+
cached_log = search_similar_execution(
|
|
60
|
+
query_vector=input_vector,
|
|
61
|
+
function_name=function_name,
|
|
62
|
+
threshold=cache_threshold
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if cached_log:
|
|
66
|
+
distance = cached_log['metadata'].get('distance')
|
|
67
|
+
logger.info(
|
|
68
|
+
f"[Cache Hit] '{function_name}' skipped. "
|
|
69
|
+
f"Distance: {distance:.4f}, "
|
|
70
|
+
f"Trace ID: {cached_log['uuid'][:8]}..."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# (D) Log CACHE_HIT event to DB (Analysis Requirement)
|
|
74
|
+
try:
|
|
75
|
+
batch_manager = get_batch_manager()
|
|
76
|
+
|
|
77
|
+
# 1. Resolve Trace Context
|
|
78
|
+
tracer = current_tracer_var.get()
|
|
79
|
+
parent_span_id = current_span_id_var.get()
|
|
80
|
+
|
|
81
|
+
if tracer:
|
|
82
|
+
trace_id = tracer.trace_id
|
|
83
|
+
else:
|
|
84
|
+
# If cache hit happens at the root (no active tracer), generate new ID
|
|
85
|
+
trace_id = str(uuid4())
|
|
86
|
+
|
|
87
|
+
# 2. Calculate function UUID (to link with metadata)
|
|
88
|
+
module_name = getattr(func, "__module__", "__main__")
|
|
89
|
+
func_identifier = f"{module_name}.{function_name}"
|
|
90
|
+
func_uuid = generate_uuid5(func_identifier)
|
|
91
|
+
|
|
92
|
+
# 3. Prepare properties
|
|
93
|
+
hit_properties = {
|
|
94
|
+
"trace_id": trace_id,
|
|
95
|
+
"span_id": str(uuid4()),
|
|
96
|
+
"parent_span_id": parent_span_id,
|
|
97
|
+
"function_name": function_name,
|
|
98
|
+
"function_uuid": func_uuid,
|
|
99
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
100
|
+
"duration_ms": 0.0, # Zero duration for cache hits
|
|
101
|
+
"status": "CACHE_HIT",
|
|
102
|
+
"return_value": cached_log.get('return_value'),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# 4. Inject Global/Custom Tags
|
|
106
|
+
if settings.global_custom_values:
|
|
107
|
+
hit_properties.update(settings.global_custom_values)
|
|
108
|
+
|
|
109
|
+
if settings.custom_properties:
|
|
110
|
+
for key in settings.custom_properties:
|
|
111
|
+
if key in kwargs:
|
|
112
|
+
# Simple assignment for tags (complex masking not applied here for simplicity/perf)
|
|
113
|
+
hit_properties[key] = kwargs[key]
|
|
114
|
+
|
|
115
|
+
# 5. Add to batch (Async logging)
|
|
116
|
+
batch_manager.add_object(
|
|
117
|
+
collection=settings.EXECUTION_COLLECTION_NAME,
|
|
118
|
+
properties=hit_properties,
|
|
119
|
+
vector=input_vector # Reuse the query vector
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
except Exception as log_e:
|
|
123
|
+
# Non-blocking error: logging failure should not prevent returning the cached result
|
|
124
|
+
logger.error(f"Failed to log CACHE_HIT for '{function_name}': {log_e}")
|
|
125
|
+
|
|
126
|
+
# (E) Deserialize and return the cached result
|
|
127
|
+
return _deserialize_return_value(cached_log.get('return_value'))
|
|
128
|
+
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Failed to check semantic cache for '{function_name}': {e}", exc_info=True)
|
|
133
|
+
# Cache failure must not stop execution, so return None to proceed to actual function call.
|
|
134
|
+
return None
|
|
@@ -28,6 +28,7 @@ src/tests/search/test_execution_search.py
|
|
|
28
28
|
src/tests/utils/__init__.py
|
|
29
29
|
src/tests/utils/test_function_cahe.py
|
|
30
30
|
src/tests/utils/test_replayer.py
|
|
31
|
+
src/tests/utils/test_return_caching.py
|
|
31
32
|
src/tests/utils/test_status.py
|
|
32
33
|
src/tests/vectorizer/__init__.py
|
|
33
34
|
src/vectorwave/__init__.py
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from typing import Optional, Tuple, Any, Dict, Callable
|
|
3
|
-
import json
|
|
4
|
-
import inspect
|
|
5
|
-
from functools import wraps
|
|
6
|
-
|
|
7
|
-
from weaviate.util import generate_uuid5
|
|
8
|
-
|
|
9
|
-
from ..models.db_config import get_weaviate_settings, WeaviateSettings
|
|
10
|
-
from ..monitoring.tracer import _create_input_vector_data, _deserialize_return_value
|
|
11
|
-
from ..database.db_search import search_similar_execution
|
|
12
|
-
from ..vectorizer.factory import get_vectorizer
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _check_and_return_cached_result(
|
|
18
|
-
func: Callable,
|
|
19
|
-
args: Tuple[Any, ...],
|
|
20
|
-
kwargs: Dict[str, Any],
|
|
21
|
-
function_name: str,
|
|
22
|
-
cache_threshold: float,
|
|
23
|
-
is_async: bool
|
|
24
|
-
) -> Optional[Any]:
|
|
25
|
-
"""
|
|
26
|
-
Checks the VectorWaveExecutions database for a semantically similar cached result.
|
|
27
|
-
If found, returns the deserialized result; otherwise, returns None.
|
|
28
|
-
|
|
29
|
-
This function should be called from the outer_wrapper of the @vectorize decorator.
|
|
30
|
-
"""
|
|
31
|
-
if not cache_threshold: # Check if caching is effectively disabled (threshold 0 or None)
|
|
32
|
-
return None
|
|
33
|
-
|
|
34
|
-
settings: WeaviateSettings = get_weaviate_settings()
|
|
35
|
-
vectorizer = get_vectorizer()
|
|
36
|
-
|
|
37
|
-
if vectorizer is None:
|
|
38
|
-
# This case should ideally be caught in decorator setup, but defensive check remains.
|
|
39
|
-
logger.error(f"Cannot perform vectorization for caching on '{function_name}': Vectorizer is None.")
|
|
40
|
-
return None
|
|
41
|
-
|
|
42
|
-
try:
|
|
43
|
-
# (A) Create vectorization data
|
|
44
|
-
input_vector_data = _create_input_vector_data(
|
|
45
|
-
func_name=function_name,
|
|
46
|
-
args=args, # Original positional args
|
|
47
|
-
kwargs=kwargs, # Original keyword args
|
|
48
|
-
sensitive_keys=settings.sensitive_keys
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
# (B) Vectorize (Synchronous call is fine, as vectorizer.embed is sync)
|
|
52
|
-
input_vector = vectorizer.embed(input_vector_data['text'])
|
|
53
|
-
|
|
54
|
-
# (C) Search Cache (Needs to handle the fact that search_similar_execution is sync)
|
|
55
|
-
cached_log = search_similar_execution(
|
|
56
|
-
query_vector=input_vector,
|
|
57
|
-
function_name=function_name,
|
|
58
|
-
threshold=cache_threshold
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
if cached_log:
|
|
62
|
-
distance = cached_log['metadata'].get('distance')
|
|
63
|
-
logger.info(
|
|
64
|
-
f"[Cache Hit] '{function_name}' skipped. "
|
|
65
|
-
f"Distance: {distance:.4f}, "
|
|
66
|
-
f"Trace ID: {cached_log['uuid'][:8]}..."
|
|
67
|
-
)
|
|
68
|
-
# (D) Deserialize and return the cached result
|
|
69
|
-
return _deserialize_return_value(cached_log.get('return_value'))
|
|
70
|
-
|
|
71
|
-
return None
|
|
72
|
-
|
|
73
|
-
except Exception as e:
|
|
74
|
-
logger.error(f"Failed to check semantic cache for '{function_name}': {e}", exc_info=True)
|
|
75
|
-
# Cache failure must not stop execution, so return None to proceed to actual function call.
|
|
76
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|