vectorwave 0.2.2__tar.gz → 0.2.4__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.2.2/src/vectorwave.egg-info → vectorwave-0.2.4}/PKG-INFO +35 -1
- {vectorwave-0.2.2 → vectorwave-0.2.4}/Readme.md +34 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/pyproject.toml +1 -1
- vectorwave-0.2.4/src/tests/core/test_auto_injector.py +228 -0
- vectorwave-0.2.4/src/tests/core/test_auto_injector_real.py +111 -0
- vectorwave-0.2.4/src/tests/utils/test_replay_source.py +140 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/__init__.py +3 -2
- vectorwave-0.2.4/src/vectorwave/core/auto_injector.py +100 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/core/decorator.py +9 -4
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/database/db.py +5 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/monitoring/tracer.py +3 -1
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/search/execution_search.py +31 -0
- vectorwave-0.2.4/src/vectorwave/utils/context.py +4 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/utils/replayer.py +8 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/utils/replayer_semantic.py +6 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4/src/vectorwave.egg-info}/PKG-INFO +35 -1
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave.egg-info/SOURCES.txt +5 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/LICENSE +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/MANIFEST.in +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/NOTICE +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/setup.cfg +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/batch/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/batch/test_batch.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/conftest.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/core/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/core/llm/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/core/llm/test_token_usage.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/core/test_decorator.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/core/test_semantic_caching.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/database/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/database/test_archiver.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/database/test_dataset.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/database/test_db.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/database/test_db_search.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/exception/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/models/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/models/test_db_config.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/monitoring/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/monitoring/alert/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/monitoring/alert/test_alerter.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/monitoring/test_async_trace.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/monitoring/test_tracer.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/prediction/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/search/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/search/test_execution_search.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/utils/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/utils/test_function_cahe.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/utils/test_replayer.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/utils/test_return_caching.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/utils/test_status.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/tests/vectorizer/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/batch/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/batch/batch.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/core/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/core/core.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/core/generator.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/core/llm/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/core/llm/base.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/core/llm/factory.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/core/llm/openai_client.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/database/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/database/archiver.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/database/dataset.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/database/db_search.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/exception/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/exception/exceptions.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/models/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/models/db_config.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/monitoring/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/monitoring/alert/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/monitoring/alert/base.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/monitoring/alert/factory.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/monitoring/alert/null_alerter.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/monitoring/alert/webhook_alerter.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/monitoring/monitoring.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/prediction/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/prediction/predictor.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/search/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/search/extended_search.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/search/rag_search.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/utils/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/utils/function_cache.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/utils/healer.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/utils/return_caching_utils.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/utils/status.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/vectorizer/__init__.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/vectorizer/base.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/vectorizer/factory.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/vectorizer/huggingface_vectorizer.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave/vectorizer/openai_vectorizer.py +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave.egg-info/dependency_links.txt +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave.egg-info/requires.txt +0 -0
- {vectorwave-0.2.2 → vectorwave-0.2.4}/src/vectorwave.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vectorwave
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: VectorWave: Seamless Auto-Vectorization Framework
|
|
5
5
|
Author-email: junyeonggim <junyeonggim5@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -618,6 +618,40 @@ archiver.export_and_clear(
|
|
|
618
618
|
{"messages": [{"role": "user", "content": "{\"price\": 100, \"tax\": 0.1}"}, {"role": "assistant", "content": "110.0"}]}
|
|
619
619
|
```
|
|
620
620
|
|
|
621
|
+
## 🌊 Auto-Injection (Zero-Code Change Integration)
|
|
622
|
+
|
|
623
|
+
You don't need to modify your business logic to use VectorWave. Use `VectorWaveAutoInjector` to inject functionality externally.
|
|
624
|
+
|
|
625
|
+
### How to use
|
|
626
|
+
|
|
627
|
+
1. **Configure Global Settings**: Set default values like `team`, `priority`, or `auto` (pending mode).
|
|
628
|
+
2. **Inject Modules**: Specify the target module path string.
|
|
629
|
+
|
|
630
|
+
```python
|
|
631
|
+
from vectorwave import initialize_database, VectorWaveAutoInjector, generate_and_register_metadata
|
|
632
|
+
|
|
633
|
+
# 1. Initialize DB
|
|
634
|
+
initialize_database()
|
|
635
|
+
|
|
636
|
+
# 2. Configure AutoInjector (Global Settings)
|
|
637
|
+
VectorWaveAutoInjector.configure(
|
|
638
|
+
team="billing-team",
|
|
639
|
+
priority=1,
|
|
640
|
+
auto=True # True: Collect metadata in memory (Pending), False: Save immediately to DB
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
# 3. Inject VectorWave into your module
|
|
644
|
+
# (No need to add @vectorize decorators in 'my_service.payment' code!)
|
|
645
|
+
VectorWaveAutoInjector.inject("my_service.payment")
|
|
646
|
+
|
|
647
|
+
# 4. Register Metadata (If auto=True)
|
|
648
|
+
generate_and_register_metadata()
|
|
649
|
+
|
|
650
|
+
# 5. Run your business logic
|
|
651
|
+
import my_service.payment
|
|
652
|
+
my_service.payment.process_transaction()
|
|
653
|
+
```
|
|
654
|
+
|
|
621
655
|
## 🤝 Contributing
|
|
622
656
|
|
|
623
657
|
We welcome all forms of contributions, including bug reports, feature requests, and code contributions. Please refer to [CONTRIBUTING.md](https://www.google.com/search?q=httpsS://www.google.com/search%3Fq%3DCONTRIBUTING.md) for details.
|
|
@@ -592,6 +592,40 @@ archiver.export_and_clear(
|
|
|
592
592
|
{"messages": [{"role": "user", "content": "{\"price\": 100, \"tax\": 0.1}"}, {"role": "assistant", "content": "110.0"}]}
|
|
593
593
|
```
|
|
594
594
|
|
|
595
|
+
## 🌊 Auto-Injection (Zero-Code Change Integration)
|
|
596
|
+
|
|
597
|
+
You don't need to modify your business logic to use VectorWave. Use `VectorWaveAutoInjector` to inject functionality externally.
|
|
598
|
+
|
|
599
|
+
### How to use
|
|
600
|
+
|
|
601
|
+
1. **Configure Global Settings**: Set default values like `team`, `priority`, or `auto` (pending mode).
|
|
602
|
+
2. **Inject Modules**: Specify the target module path string.
|
|
603
|
+
|
|
604
|
+
```python
|
|
605
|
+
from vectorwave import initialize_database, VectorWaveAutoInjector, generate_and_register_metadata
|
|
606
|
+
|
|
607
|
+
# 1. Initialize DB
|
|
608
|
+
initialize_database()
|
|
609
|
+
|
|
610
|
+
# 2. Configure AutoInjector (Global Settings)
|
|
611
|
+
VectorWaveAutoInjector.configure(
|
|
612
|
+
team="billing-team",
|
|
613
|
+
priority=1,
|
|
614
|
+
auto=True # True: Collect metadata in memory (Pending), False: Save immediately to DB
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# 3. Inject VectorWave into your module
|
|
618
|
+
# (No need to add @vectorize decorators in 'my_service.payment' code!)
|
|
619
|
+
VectorWaveAutoInjector.inject("my_service.payment")
|
|
620
|
+
|
|
621
|
+
# 4. Register Metadata (If auto=True)
|
|
622
|
+
generate_and_register_metadata()
|
|
623
|
+
|
|
624
|
+
# 5. Run your business logic
|
|
625
|
+
import my_service.payment
|
|
626
|
+
my_service.payment.process_transaction()
|
|
627
|
+
```
|
|
628
|
+
|
|
595
629
|
## 🤝 Contributing
|
|
596
630
|
|
|
597
631
|
We welcome all forms of contributions, including bug reports, feature requests, and code contributions. Please refer to [CONTRIBUTING.md](https://www.google.com/search?q=httpsS://www.google.com/search%3Fq%3DCONTRIBUTING.md) for details.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import sys
|
|
3
|
+
import types
|
|
4
|
+
from unittest.mock import MagicMock, ANY
|
|
5
|
+
from vectorwave.core.auto_injector import VectorWaveAutoInjector, create_smart_wrapper
|
|
6
|
+
|
|
7
|
+
# --- Fixtures ---
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_target_module():
|
|
11
|
+
"""Dynamically creates a fake module for testing."""
|
|
12
|
+
module_name = "fake_business_logic"
|
|
13
|
+
mod = types.ModuleType(module_name)
|
|
14
|
+
|
|
15
|
+
# Define functions for testing
|
|
16
|
+
def func_a(x): return x + 1
|
|
17
|
+
def func_b(y): return y * 2
|
|
18
|
+
|
|
19
|
+
# Set __module__ attribute so the Injector recognizes them as 'functions of this module'
|
|
20
|
+
func_a.__module__ = module_name
|
|
21
|
+
func_b.__module__ = module_name
|
|
22
|
+
|
|
23
|
+
mod.func_a = func_a
|
|
24
|
+
mod.func_b = func_b
|
|
25
|
+
|
|
26
|
+
# Register to sys.modules
|
|
27
|
+
sys.modules[module_name] = mod
|
|
28
|
+
yield mod
|
|
29
|
+
# Cleanup
|
|
30
|
+
del sys.modules[module_name]
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def mock_deps(monkeypatch):
|
|
34
|
+
"""
|
|
35
|
+
Mocks vectorize, trace_span, and contextvar that auto_injector depends on.
|
|
36
|
+
"""
|
|
37
|
+
mock_vectorize = MagicMock()
|
|
38
|
+
mock_trace_span = MagicMock()
|
|
39
|
+
mock_tracer_var = MagicMock()
|
|
40
|
+
|
|
41
|
+
# Mock vectorize: @vectorize(...) -> decorator -> returns root_wrapper
|
|
42
|
+
def vectorize_side_effect(**kwargs):
|
|
43
|
+
def decorator(func):
|
|
44
|
+
# Return a mock acting as root_wrapper (Default behavior: execute original function)
|
|
45
|
+
root_wrapper = MagicMock(side_effect=func)
|
|
46
|
+
return root_wrapper
|
|
47
|
+
return decorator
|
|
48
|
+
|
|
49
|
+
# [Modified] Mock trace_span: Modified to accept positional arguments (*args)
|
|
50
|
+
def trace_span_side_effect(*args, **kwargs):
|
|
51
|
+
# Case 1: Called as trace_span(func, capture_return_value=True)
|
|
52
|
+
if args and callable(args[0]):
|
|
53
|
+
func = args[0]
|
|
54
|
+
def wrapper(*w_args, **w_kwargs):
|
|
55
|
+
return func(*w_args, **w_kwargs)
|
|
56
|
+
return wrapper
|
|
57
|
+
|
|
58
|
+
# Case 2: Called as a decorator factory @trace_span(...)
|
|
59
|
+
def decorator(func):
|
|
60
|
+
def wrapper(*w_args, **w_kwargs):
|
|
61
|
+
return func(*w_args, **w_kwargs)
|
|
62
|
+
return wrapper
|
|
63
|
+
return decorator
|
|
64
|
+
|
|
65
|
+
mock_vectorize.side_effect = vectorize_side_effect
|
|
66
|
+
mock_trace_span.side_effect = trace_span_side_effect
|
|
67
|
+
|
|
68
|
+
# Patch according to the module path
|
|
69
|
+
monkeypatch.setattr("vectorwave.core.auto_injector.vectorize", mock_vectorize)
|
|
70
|
+
monkeypatch.setattr("vectorwave.core.auto_injector.trace_span", mock_trace_span)
|
|
71
|
+
monkeypatch.setattr("vectorwave.core.auto_injector.current_tracer_var", mock_tracer_var)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"vectorize": mock_vectorize,
|
|
75
|
+
"trace_span": mock_trace_span,
|
|
76
|
+
"tracer_var": mock_tracer_var
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@pytest.fixture
|
|
80
|
+
def mock_multiple_modules():
|
|
81
|
+
"""Dynamically creates two different fake modules (Payment, Auth)."""
|
|
82
|
+
name_a = "service_payment"
|
|
83
|
+
name_b = "service_auth"
|
|
84
|
+
|
|
85
|
+
mod_a = types.ModuleType(name_a)
|
|
86
|
+
mod_b = types.ModuleType(name_b)
|
|
87
|
+
|
|
88
|
+
def pay(x): return x
|
|
89
|
+
def login(x): return x
|
|
90
|
+
|
|
91
|
+
pay.__module__ = name_a
|
|
92
|
+
login.__module__ = name_b
|
|
93
|
+
|
|
94
|
+
mod_a.process_payment = pay
|
|
95
|
+
mod_b.login_user = login
|
|
96
|
+
|
|
97
|
+
sys.modules[name_a] = mod_a
|
|
98
|
+
sys.modules[name_b] = mod_b
|
|
99
|
+
|
|
100
|
+
yield mod_a, mod_b
|
|
101
|
+
|
|
102
|
+
del sys.modules[name_a]
|
|
103
|
+
del sys.modules[name_b]
|
|
104
|
+
|
|
105
|
+
# --- Test Cases ---
|
|
106
|
+
|
|
107
|
+
def test_inject_calls_vectorize_immediately(mock_target_module, mock_deps):
|
|
108
|
+
"""
|
|
109
|
+
Case 1: Verify that vectorize is called immediately at injection time to register metadata.
|
|
110
|
+
"""
|
|
111
|
+
# Act
|
|
112
|
+
VectorWaveAutoInjector.inject("fake_business_logic", team="test-team")
|
|
113
|
+
|
|
114
|
+
# Assert
|
|
115
|
+
# 1. vectorize should have been called (Immediate registration)
|
|
116
|
+
mock_deps["vectorize"].assert_called()
|
|
117
|
+
|
|
118
|
+
# 2. Verify passed configuration
|
|
119
|
+
call_kwargs = mock_deps["vectorize"].call_args.kwargs
|
|
120
|
+
assert call_kwargs["team"] == "test-team"
|
|
121
|
+
|
|
122
|
+
# 3. Verify the function is wrapped
|
|
123
|
+
assert getattr(mock_target_module.func_a, "_is_vectorized") is True
|
|
124
|
+
|
|
125
|
+
def test_smart_wrapper_root_execution(mock_target_module, mock_deps):
|
|
126
|
+
"""
|
|
127
|
+
Case 2: Verify that the pre-created root_wrapper is executed when there is no tracer (Root).
|
|
128
|
+
"""
|
|
129
|
+
# Arrange
|
|
130
|
+
mock_deps["tracer_var"].get.return_value = None # No parent trace
|
|
131
|
+
|
|
132
|
+
# [Modified] Remove fixture's side_effect so return_value setting takes effect.
|
|
133
|
+
mock_deps["vectorize"].side_effect = None
|
|
134
|
+
|
|
135
|
+
# vectorize(...) -> decorator_mock returns -> decorator_mock(func) -> root_wrapper_mock returns
|
|
136
|
+
decorator_mock = MagicMock()
|
|
137
|
+
root_wrapper_mock = MagicMock(return_value=11)
|
|
138
|
+
|
|
139
|
+
mock_deps["vectorize"].return_value = decorator_mock
|
|
140
|
+
decorator_mock.return_value = root_wrapper_mock
|
|
141
|
+
|
|
142
|
+
VectorWaveAutoInjector.inject("fake_business_logic")
|
|
143
|
+
|
|
144
|
+
# Act
|
|
145
|
+
result = mock_target_module.func_a(10)
|
|
146
|
+
|
|
147
|
+
# Assert
|
|
148
|
+
assert result == 11
|
|
149
|
+
# root_wrapper should have been called
|
|
150
|
+
root_wrapper_mock.assert_called_once()
|
|
151
|
+
# trace_span should not be called (logic is already inside root_wrapper)
|
|
152
|
+
mock_deps["trace_span"].assert_not_called()
|
|
153
|
+
|
|
154
|
+
def test_smart_wrapper_child_execution(mock_target_module, mock_deps):
|
|
155
|
+
"""
|
|
156
|
+
Case 3: Verify that trace_span is dynamically called when there is a tracer (Child).
|
|
157
|
+
"""
|
|
158
|
+
# Arrange
|
|
159
|
+
mock_deps["tracer_var"].get.return_value = MagicMock() # Parent trace exists
|
|
160
|
+
|
|
161
|
+
# [Modified] Remove side_effect and set Mock
|
|
162
|
+
mock_deps["vectorize"].side_effect = None
|
|
163
|
+
|
|
164
|
+
decorator_mock = MagicMock()
|
|
165
|
+
root_wrapper_mock = MagicMock(return_value=999) # Should not be called
|
|
166
|
+
|
|
167
|
+
mock_deps["vectorize"].return_value = decorator_mock
|
|
168
|
+
decorator_mock.return_value = root_wrapper_mock
|
|
169
|
+
|
|
170
|
+
VectorWaveAutoInjector.inject("fake_business_logic")
|
|
171
|
+
|
|
172
|
+
# Act
|
|
173
|
+
# trace_span mock is configured to call the original function (see fixture)
|
|
174
|
+
# func_b: y * 2 -> 5 * 2 = 10
|
|
175
|
+
result = mock_target_module.func_b(5)
|
|
176
|
+
|
|
177
|
+
# Assert
|
|
178
|
+
assert result == 10
|
|
179
|
+
# trace_span should have been called
|
|
180
|
+
mock_deps["trace_span"].assert_called()
|
|
181
|
+
# root_wrapper should not have been called
|
|
182
|
+
root_wrapper_mock.assert_not_called()
|
|
183
|
+
|
|
184
|
+
def test_prevent_double_injection(mock_target_module, mock_deps):
|
|
185
|
+
"""
|
|
186
|
+
Case 4: Verify that calling inject again on an already injected function does not result in double wrapping.
|
|
187
|
+
"""
|
|
188
|
+
# 1. First Injection
|
|
189
|
+
VectorWaveAutoInjector.inject("fake_business_logic")
|
|
190
|
+
first_wrapped_func = mock_target_module.func_a
|
|
191
|
+
call_count_first = mock_deps["vectorize"].call_count
|
|
192
|
+
|
|
193
|
+
# 2. Second Injection
|
|
194
|
+
VectorWaveAutoInjector.inject("fake_business_logic")
|
|
195
|
+
second_wrapped_func = mock_target_module.func_a
|
|
196
|
+
call_count_second = mock_deps["vectorize"].call_count
|
|
197
|
+
|
|
198
|
+
# Assert
|
|
199
|
+
# Object addresses must be identical
|
|
200
|
+
assert first_wrapped_func is second_wrapped_func
|
|
201
|
+
# vectorize call count should not increase (Prevent duplicate registration)
|
|
202
|
+
assert call_count_first == call_count_second
|
|
203
|
+
|
|
204
|
+
def test_inject_using_config_dictionary_pattern(mock_multiple_modules, mock_deps):
|
|
205
|
+
"""
|
|
206
|
+
Case 5: Verify that configurations are correctly applied when injecting multiple modules in a batch.
|
|
207
|
+
"""
|
|
208
|
+
mod_a, mod_b = mock_multiple_modules
|
|
209
|
+
|
|
210
|
+
# Injection
|
|
211
|
+
VectorWaveAutoInjector.configure(team="default") # Global config example
|
|
212
|
+
|
|
213
|
+
VectorWaveAutoInjector.inject("service_payment", priority=1)
|
|
214
|
+
VectorWaveAutoInjector.inject("service_auth", team="security")
|
|
215
|
+
|
|
216
|
+
# Verify: Check arguments passed to vectorize calls
|
|
217
|
+
calls = mock_deps["vectorize"].call_args_list
|
|
218
|
+
# Collect kwargs
|
|
219
|
+
configs = [c.kwargs for c in calls]
|
|
220
|
+
|
|
221
|
+
# service_payment: default team + priority 1
|
|
222
|
+
config_a = next((c for c in configs if c.get("priority") == 1), None)
|
|
223
|
+
assert config_a is not None
|
|
224
|
+
assert config_a["team"] == "default"
|
|
225
|
+
|
|
226
|
+
# service_auth: security team (override)
|
|
227
|
+
config_b = next((c for c in configs if c.get("team") == "security"), None)
|
|
228
|
+
assert config_b is not None
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import sys
|
|
3
|
+
import types
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
|
|
6
|
+
# Import actual modules
|
|
7
|
+
from vectorwave import vectorize, VectorWaveAutoInjector
|
|
8
|
+
|
|
9
|
+
# --- Fixtures ---
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_db_dependencies():
|
|
13
|
+
"""
|
|
14
|
+
Mocks heavy dependencies to prevent @vectorize decorator from internally calling
|
|
15
|
+
DB or AI models.
|
|
16
|
+
(The wrapping logic itself runs for 'real')
|
|
17
|
+
"""
|
|
18
|
+
with patch("vectorwave.core.decorator.get_batch_manager") as mock_batch, \
|
|
19
|
+
patch("vectorwave.core.decorator.get_vectorizer") as mock_vec, \
|
|
20
|
+
patch("vectorwave.core.decorator.function_cache_manager"), \
|
|
21
|
+
patch("vectorwave.core.decorator.get_weaviate_settings"), \
|
|
22
|
+
patch("vectorwave.core.decorator.generate_uuid5", return_value="mock-uuid"):
|
|
23
|
+
|
|
24
|
+
# Return fake DB settings (prevent errors)
|
|
25
|
+
mock_settings = MagicMock()
|
|
26
|
+
mock_settings.custom_properties = {}
|
|
27
|
+
mock_settings.COLLECTION_NAME = "VectorWaveFunction"
|
|
28
|
+
patch("vectorwave.core.decorator.get_weaviate_settings", return_value=mock_settings).start()
|
|
29
|
+
|
|
30
|
+
yield
|
|
31
|
+
|
|
32
|
+
# --- Real Function Integration Test ---
|
|
33
|
+
|
|
34
|
+
def test_real_function_mixed_usage(mock_db_dependencies):
|
|
35
|
+
"""
|
|
36
|
+
[Integration Test] Verifies that double wrapping is prevented when mixing
|
|
37
|
+
actual @vectorize decorator and AutoInjector using real function objects.
|
|
38
|
+
"""
|
|
39
|
+
# 1. Create a fake module (acts like a real Python module)
|
|
40
|
+
module_name = "my_real_test_module"
|
|
41
|
+
mod = types.ModuleType(module_name)
|
|
42
|
+
|
|
43
|
+
# -------------------------------------------------------
|
|
44
|
+
# (A) Manually vectorized function (Using Real Decorator)
|
|
45
|
+
# -------------------------------------------------------
|
|
46
|
+
@vectorize(team="manual_team", auto=True)
|
|
47
|
+
def manual_func(x):
|
|
48
|
+
return x + 1
|
|
49
|
+
|
|
50
|
+
# -------------------------------------------------------
|
|
51
|
+
# (B) Plain function without any decorator
|
|
52
|
+
# -------------------------------------------------------
|
|
53
|
+
def plain_func(y):
|
|
54
|
+
return y * 2
|
|
55
|
+
|
|
56
|
+
# [Important] Set __module__ so Injector recognizes them as internal functions
|
|
57
|
+
manual_func.__module__ = module_name
|
|
58
|
+
plain_func.__module__ = module_name
|
|
59
|
+
|
|
60
|
+
# Register functions to the module
|
|
61
|
+
mod.manual_func = manual_func
|
|
62
|
+
mod.plain_func = plain_func
|
|
63
|
+
|
|
64
|
+
# Register to sys.modules so importlib can find it
|
|
65
|
+
sys.modules[module_name] = mod
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# 2. Check state before injection
|
|
69
|
+
print("\n[Before Injection]")
|
|
70
|
+
print(f"manual_func vectorized? {getattr(mod.manual_func, '_is_vectorized', False)}")
|
|
71
|
+
print(f"plain_func vectorized? {getattr(mod.plain_func, '_is_vectorized', False)}")
|
|
72
|
+
|
|
73
|
+
# manual_func should already have the decorator applied
|
|
74
|
+
assert getattr(mod.manual_func, "_is_vectorized", False) is True
|
|
75
|
+
# plain_func should be a pure function
|
|
76
|
+
assert getattr(mod.plain_func, "_is_vectorized", False) is False
|
|
77
|
+
|
|
78
|
+
# Save object ID of manual_func before injection (for comparison)
|
|
79
|
+
original_manual_func_id = id(mod.manual_func)
|
|
80
|
+
|
|
81
|
+
# -------------------------------------------------------
|
|
82
|
+
# 3. Execute AutoInjector (Real Injection)
|
|
83
|
+
# -------------------------------------------------------
|
|
84
|
+
print("\n🌊 Injecting VectorWave...")
|
|
85
|
+
VectorWaveAutoInjector.inject(module_name, team="auto_team")
|
|
86
|
+
|
|
87
|
+
# 4. Verify results
|
|
88
|
+
print("\n[After Injection]")
|
|
89
|
+
|
|
90
|
+
# (A) Verify manual_func: Prevent double wrapping
|
|
91
|
+
# If AutoInjector skipped it, the object ID should remain unchanged.
|
|
92
|
+
current_manual_func_id = id(mod.manual_func)
|
|
93
|
+
|
|
94
|
+
if current_manual_func_id == original_manual_func_id:
|
|
95
|
+
print("✅ manual_func: SKIPPED (Identity preserved)")
|
|
96
|
+
else:
|
|
97
|
+
print("❌ manual_func: WRAPPED AGAIN (Identity changed!)")
|
|
98
|
+
|
|
99
|
+
assert current_manual_func_id == original_manual_func_id, \
|
|
100
|
+
"Manually configured function was double-wrapped by AutoInjector!"
|
|
101
|
+
|
|
102
|
+
# (B) Verify plain_func: Auto-wrapping success
|
|
103
|
+
# Since it's newly wrapped, _is_vectorized should become True.
|
|
104
|
+
is_vectorized = getattr(mod.plain_func, "_is_vectorized", False)
|
|
105
|
+
print(f"plain_func vectorized? {is_vectorized}")
|
|
106
|
+
|
|
107
|
+
assert is_vectorized is True, "Plain function was not auto-injected!"
|
|
108
|
+
|
|
109
|
+
finally:
|
|
110
|
+
# Cleanup module after test
|
|
111
|
+
del sys.modules[module_name]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import sys
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
from vectorwave import vectorize, search_executions
|
|
5
|
+
from vectorwave.utils.replayer import VectorWaveReplayer
|
|
6
|
+
from vectorwave.models.db_config import WeaviateSettings
|
|
7
|
+
|
|
8
|
+
# 1. Dummy function for testing
|
|
9
|
+
@vectorize(unique_id="test_replay_tagging")
|
|
10
|
+
def calc_add(a: int, b: int):
|
|
11
|
+
"""Simple addition function for testing replay tags."""
|
|
12
|
+
return a + b
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def mock_replay_deps(monkeypatch):
|
|
16
|
+
"""Mocks dependencies for Replay Source testing."""
|
|
17
|
+
# Mock Settings
|
|
18
|
+
mock_settings = WeaviateSettings(
|
|
19
|
+
EXECUTION_COLLECTION_NAME="VectorWaveExecutions",
|
|
20
|
+
GOLDEN_COLLECTION_NAME="VectorWaveGoldenDataset"
|
|
21
|
+
)
|
|
22
|
+
mock_get_settings = MagicMock(return_value=mock_settings)
|
|
23
|
+
|
|
24
|
+
# Mock Batch Manager (to verify logs)
|
|
25
|
+
mock_batch = MagicMock()
|
|
26
|
+
mock_batch.add_object = MagicMock()
|
|
27
|
+
mock_get_batch = MagicMock(return_value=mock_batch)
|
|
28
|
+
|
|
29
|
+
# Mock DB Client (for Replayer initialization)
|
|
30
|
+
mock_client = MagicMock()
|
|
31
|
+
mock_collection = MagicMock()
|
|
32
|
+
mock_client.collections.get.return_value = mock_collection
|
|
33
|
+
mock_get_client = MagicMock(return_value=mock_client)
|
|
34
|
+
|
|
35
|
+
# --- [수정] Patch dependencies where they are USED, not just defined ---
|
|
36
|
+
|
|
37
|
+
# 1. Patch for Replayer (vectorwave/utils/replayer.py)
|
|
38
|
+
monkeypatch.setattr("vectorwave.utils.replayer.get_cached_client", mock_get_client)
|
|
39
|
+
monkeypatch.setattr("vectorwave.utils.replayer.get_weaviate_settings", mock_get_settings)
|
|
40
|
+
|
|
41
|
+
# 2. Patch for Database (vectorwave/database/db.py) - 안전장치
|
|
42
|
+
monkeypatch.setattr("vectorwave.database.db.get_cached_client", mock_get_client)
|
|
43
|
+
|
|
44
|
+
# 3. Patch for Config (vectorwave/models/db_config.py) - 안전장치
|
|
45
|
+
monkeypatch.setattr("vectorwave.models.db_config.get_weaviate_settings", mock_get_settings)
|
|
46
|
+
|
|
47
|
+
# 4. Patch batch manager for Decorator & Tracer
|
|
48
|
+
monkeypatch.setattr("vectorwave.core.decorator.get_batch_manager", mock_get_batch)
|
|
49
|
+
monkeypatch.setattr("vectorwave.monitoring.tracer.get_batch_manager", mock_get_batch)
|
|
50
|
+
|
|
51
|
+
# 5. Patch search_executions in THIS test file
|
|
52
|
+
mock_search = MagicMock()
|
|
53
|
+
monkeypatch.setattr(sys.modules[__name__], "search_executions", mock_search)
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"batch": mock_batch,
|
|
57
|
+
"search": mock_search,
|
|
58
|
+
"client": mock_client
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def test_execution_source_tagging(mock_replay_deps):
|
|
62
|
+
"""
|
|
63
|
+
Verification Scenario:
|
|
64
|
+
1. Realtime Execution -> Check if saved to DB as 'REALTIME'
|
|
65
|
+
2. Replay Execution -> Check if saved to DB as 'REPLAY'
|
|
66
|
+
"""
|
|
67
|
+
mock_batch = mock_replay_deps["batch"]
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------
|
|
70
|
+
# Case 1: Realtime Execution
|
|
71
|
+
# ---------------------------------------------------------
|
|
72
|
+
print("\n🚀 1. Running Realtime Execution...")
|
|
73
|
+
calc_add(10, 20)
|
|
74
|
+
|
|
75
|
+
# Check if log was sent to BatchManager
|
|
76
|
+
assert mock_batch.add_object.called
|
|
77
|
+
|
|
78
|
+
# Get the latest call args
|
|
79
|
+
args, kwargs = mock_batch.add_object.call_args
|
|
80
|
+
props = kwargs['properties']
|
|
81
|
+
|
|
82
|
+
print(f" [Log Check] Source: {props.get('exec_source')}")
|
|
83
|
+
|
|
84
|
+
# Verify exec_source is 'REALTIME'
|
|
85
|
+
assert props.get("exec_source") == "REALTIME"
|
|
86
|
+
assert props.get("status") == "SUCCESS"
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------
|
|
89
|
+
# Case 2: Replay Execution
|
|
90
|
+
# ---------------------------------------------------------
|
|
91
|
+
print("\n🔄 2. Running Replay...")
|
|
92
|
+
|
|
93
|
+
# Prepare dummy log for Replayer to fetch
|
|
94
|
+
dummy_log_obj = MagicMock()
|
|
95
|
+
dummy_log_obj.uuid = "old-uuid-123"
|
|
96
|
+
dummy_log_obj.properties = {
|
|
97
|
+
"function_name": "calc_add",
|
|
98
|
+
"a": 10,
|
|
99
|
+
"b": 20,
|
|
100
|
+
"return_value": "30",
|
|
101
|
+
"exec_source": "REALTIME"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Replayer uses client.collections.get().query.fetch_objects()
|
|
105
|
+
mock_collection = mock_replay_deps["client"].collections.get.return_value
|
|
106
|
+
mock_collection.query.fetch_objects.return_value.objects = [dummy_log_obj]
|
|
107
|
+
|
|
108
|
+
# Also need to mock import_module since Replayer imports the function dynamically
|
|
109
|
+
with patch("vectorwave.utils.replayer.importlib.import_module") as mock_import:
|
|
110
|
+
mock_module = MagicMock()
|
|
111
|
+
# Set the function on the mock module
|
|
112
|
+
setattr(mock_module, "calc_add", calc_add)
|
|
113
|
+
mock_import.return_value = mock_module
|
|
114
|
+
|
|
115
|
+
replayer = VectorWaveReplayer()
|
|
116
|
+
|
|
117
|
+
# Reset batch mock to capture only replay log
|
|
118
|
+
mock_batch.add_object.reset_mock()
|
|
119
|
+
|
|
120
|
+
# Execute Replay
|
|
121
|
+
result = replayer.replay(
|
|
122
|
+
function_full_name="src.tests.utils.test_replay_source.calc_add",
|
|
123
|
+
limit=1
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
assert result['total'] > 0, "Replay should have executed at least 1 test."
|
|
127
|
+
|
|
128
|
+
# Check if new log was sent to BatchManager
|
|
129
|
+
assert mock_batch.add_object.called
|
|
130
|
+
|
|
131
|
+
# Get the latest call args (from Replay execution)
|
|
132
|
+
args, kwargs = mock_batch.add_object.call_args
|
|
133
|
+
replay_props = kwargs['properties']
|
|
134
|
+
|
|
135
|
+
print(f" [Log Check] Source: {replay_props.get('exec_source')}")
|
|
136
|
+
|
|
137
|
+
# Verify exec_source is 'REPLAY'
|
|
138
|
+
assert replay_props.get("exec_source") == "REPLAY"
|
|
139
|
+
|
|
140
|
+
print("\n✅ All tests passed! Execution Source tagging is working.")
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from .core.decorator import vectorize
|
|
2
|
-
|
|
3
2
|
from .database.db import initialize_database
|
|
4
3
|
from .database.db_search import search_functions, search_executions, search_errors_by_message, search_functions_hybrid
|
|
5
4
|
from .monitoring.tracer import trace_span
|
|
@@ -9,6 +8,7 @@ from .utils.healer import VectorWaveHealer
|
|
|
9
8
|
from .utils.replayer import VectorWaveReplayer
|
|
10
9
|
from .utils.replayer_semantic import SemanticReplayer
|
|
11
10
|
from .database.dataset import VectorWaveDatasetManager
|
|
11
|
+
from .core.auto_injector import VectorWaveAutoInjector
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
14
14
|
'vectorize',
|
|
@@ -24,5 +24,6 @@ __all__ = [
|
|
|
24
24
|
'VectorWaveHealer',
|
|
25
25
|
'VectorWaveReplayer',
|
|
26
26
|
'SemanticReplayer',
|
|
27
|
-
'VectorWaveDatasetManager'
|
|
27
|
+
'VectorWaveDatasetManager',
|
|
28
|
+
'VectorWaveAutoInjector'
|
|
28
29
|
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import importlib
|
|
3
|
+
import logging
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
# VectorWave Core Modules
|
|
8
|
+
from .decorator import vectorize
|
|
9
|
+
from ..monitoring.tracer import trace_span, current_tracer_var
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
def create_smart_wrapper(original_func, root_wrapper):
|
|
14
|
+
"""
|
|
15
|
+
Creates a wrapper that switches behavior based on the Tracing Context.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
original_func: The original function (used when acting as a Child Span).
|
|
19
|
+
root_wrapper: The wrapper generated by vectorize (used when acting as a Root Span, includes metadata).
|
|
20
|
+
"""
|
|
21
|
+
@wraps(original_func)
|
|
22
|
+
def wrapper(*args, **kwargs):
|
|
23
|
+
# Check for existing tracer at runtime
|
|
24
|
+
tracer = current_tracer_var.get()
|
|
25
|
+
|
|
26
|
+
if tracer:
|
|
27
|
+
# [Case A] Parent trace exists -> Act as Child Span
|
|
28
|
+
# (Wrap original function with trace_span and execute)
|
|
29
|
+
return trace_span(original_func, capture_return_value=True)(*args, **kwargs)
|
|
30
|
+
else:
|
|
31
|
+
# [Case B] No parent trace -> Act as Root Span
|
|
32
|
+
# (Delegate to the already configured root_wrapper)
|
|
33
|
+
return root_wrapper(*args, **kwargs)
|
|
34
|
+
|
|
35
|
+
# Mark to prevent double injection
|
|
36
|
+
wrapper._is_vectorized = True
|
|
37
|
+
return wrapper
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class VectorWaveAutoInjector:
|
|
41
|
+
"""
|
|
42
|
+
Scans specified modules or packages to automatically inject (Monkey Patch) VectorWave functionality.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# Global default configuration
|
|
46
|
+
_default_config = {}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def configure(cls, **kwargs):
|
|
50
|
+
"""
|
|
51
|
+
Sets the default configuration to be applied to all inject calls.
|
|
52
|
+
"""
|
|
53
|
+
cls._default_config.update(kwargs)
|
|
54
|
+
logger.info(f"⚙️ VectorWave AutoInjector Configured: {cls._default_config}")
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def inject(cls, target_module_path: str, recursive: bool = False, **config):
|
|
58
|
+
"""
|
|
59
|
+
Finds functions within a module and injects VectorWave functionality.
|
|
60
|
+
**Metadata registration (DB save or Pending) is performed immediately at this point.**
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
module = importlib.import_module(target_module_path)
|
|
64
|
+
except ImportError as e:
|
|
65
|
+
logger.error(f"Failed to import module '{target_module_path}': {e}")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Merge configurations (Defaults < Individual Config)
|
|
69
|
+
final_config = cls._default_config.copy()
|
|
70
|
+
final_config.update(config)
|
|
71
|
+
|
|
72
|
+
logger.info(f"🌊 [AutoInjector] Injecting VectorWave into: {module.__name__}")
|
|
73
|
+
|
|
74
|
+
patched_count = 0
|
|
75
|
+
|
|
76
|
+
for name, obj in inspect.getmembers(module):
|
|
77
|
+
# Target only functions defined within the module
|
|
78
|
+
if inspect.isfunction(obj) and obj.__module__ == module.__name__:
|
|
79
|
+
|
|
80
|
+
# Skip if already processed
|
|
81
|
+
if getattr(obj, "_is_vectorized", False):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
logger.info(f" └─ Auto-wiring: {name}()")
|
|
85
|
+
|
|
86
|
+
# [Key Change] Call vectorize immediately at injection time to perform metadata registration!
|
|
87
|
+
# - If auto=False: Immediate DB save
|
|
88
|
+
# - If auto=True: Add to PENDING list
|
|
89
|
+
# The returned root_wrapper contains the execution logic (Root Span)
|
|
90
|
+
root_wrapper = vectorize(**final_config)(obj)
|
|
91
|
+
|
|
92
|
+
# Create smart wrapper to judge Context at runtime
|
|
93
|
+
smart_wrapper = create_smart_wrapper(obj, root_wrapper)
|
|
94
|
+
|
|
95
|
+
# Replace (Monkey Patching)
|
|
96
|
+
setattr(module, name, smart_wrapper)
|
|
97
|
+
patched_count += 1
|
|
98
|
+
|
|
99
|
+
logger.info(f"✨ Injection Complete. {patched_count} functions registered & auto-wired.")
|
|
100
|
+
return module
|
|
@@ -11,6 +11,7 @@ from ..monitoring.tracer import trace_root, trace_span
|
|
|
11
11
|
from ..utils.function_cache import function_cache_manager
|
|
12
12
|
from ..utils.return_caching_utils import _check_and_return_cached_result
|
|
13
13
|
from ..vectorizer.factory import get_vectorizer
|
|
14
|
+
from ..utils.context import execution_source_context
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
@@ -53,7 +54,7 @@ def vectorize(search_description: Optional[str] = None,
|
|
|
53
54
|
func_uuid = generate_uuid5(func_identifier)
|
|
54
55
|
|
|
55
56
|
# Prepare attributes to capture
|
|
56
|
-
final_attributes = ['function_uuid', 'team', 'priority', 'run_id']
|
|
57
|
+
final_attributes = ['function_uuid', 'team', 'priority', 'run_id', 'exec_source']
|
|
57
58
|
if attributes_to_capture:
|
|
58
59
|
for attr in attributes_to_capture:
|
|
59
60
|
if attr not in final_attributes:
|
|
@@ -145,7 +146,7 @@ def vectorize(search_description: Optional[str] = None,
|
|
|
145
146
|
async def inner_wrapper(*args, **kwargs):
|
|
146
147
|
# Remove injected tags from kwargs before calling original func
|
|
147
148
|
clean_kwargs = {k: v for k, v in kwargs.items() if
|
|
148
|
-
k not in valid_execution_tags and k != 'function_uuid'}
|
|
149
|
+
k not in valid_execution_tags and k != 'function_uuid' and k != 'exec_source'}
|
|
149
150
|
return await func(*args, **clean_kwargs)
|
|
150
151
|
|
|
151
152
|
@wraps(func)
|
|
@@ -157,8 +158,10 @@ def vectorize(search_description: Optional[str] = None,
|
|
|
157
158
|
full_kwargs = kwargs.copy()
|
|
158
159
|
full_kwargs.update(valid_execution_tags)
|
|
159
160
|
full_kwargs['function_uuid'] = func_uuid
|
|
161
|
+
full_kwargs['exec_source'] = execution_source_context.get()
|
|
160
162
|
return await inner_wrapper(*args, **full_kwargs)
|
|
161
163
|
|
|
164
|
+
outer_wrapper._is_vectorized = True
|
|
162
165
|
return outer_wrapper
|
|
163
166
|
|
|
164
167
|
else: # Sync wrapper
|
|
@@ -167,7 +170,7 @@ def vectorize(search_description: Optional[str] = None,
|
|
|
167
170
|
@wraps(func)
|
|
168
171
|
def inner_wrapper(*args, **kwargs):
|
|
169
172
|
clean_kwargs = {k: v for k, v in kwargs.items() if
|
|
170
|
-
k not in valid_execution_tags and k != 'function_uuid'}
|
|
173
|
+
k not in valid_execution_tags and k != 'function_uuid' and k != 'exec_source'}
|
|
171
174
|
return func(*args, **clean_kwargs)
|
|
172
175
|
|
|
173
176
|
@wraps(func)
|
|
@@ -179,8 +182,10 @@ def vectorize(search_description: Optional[str] = None,
|
|
|
179
182
|
full_kwargs = kwargs.copy()
|
|
180
183
|
full_kwargs.update(valid_execution_tags)
|
|
181
184
|
full_kwargs['function_uuid'] = func_uuid
|
|
185
|
+
full_kwargs['exec_source'] = execution_source_context.get()
|
|
182
186
|
return inner_wrapper(*args, **full_kwargs)
|
|
183
187
|
|
|
188
|
+
outer_wrapper._is_vectorized = True
|
|
184
189
|
return outer_wrapper
|
|
185
190
|
|
|
186
|
-
return decorator
|
|
191
|
+
return decorator
|
|
@@ -282,6 +282,11 @@ def create_execution_schema(client: weaviate.WeaviateClient, settings: WeaviateS
|
|
|
282
282
|
name="return_value",
|
|
283
283
|
data_type=wvc.DataType.TEXT,
|
|
284
284
|
description="The JSON-serialized or str() representation of the function's return value"
|
|
285
|
+
),
|
|
286
|
+
wvc.Property(
|
|
287
|
+
name="exec_source",
|
|
288
|
+
data_type=wvc.DataType.TEXT,
|
|
289
|
+
description="Source of execution: 'REALTIME' (User traffic) or 'REPLAY' (Regression Test)"
|
|
285
290
|
)
|
|
286
291
|
]
|
|
287
292
|
|
|
@@ -15,6 +15,7 @@ from ..models.db_config import get_weaviate_settings, WeaviateSettings
|
|
|
15
15
|
from .alert.factory import get_alerter
|
|
16
16
|
from ..vectorizer.factory import get_vectorizer
|
|
17
17
|
from ..database.db_search import check_semantic_drift
|
|
18
|
+
from ..utils.context import execution_source_context
|
|
18
19
|
|
|
19
20
|
# Create module-level logger
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
@@ -137,7 +138,8 @@ def _create_span_properties(
|
|
|
137
138
|
"status": status,
|
|
138
139
|
"error_message": error_msg,
|
|
139
140
|
"error_code": error_code,
|
|
140
|
-
"return_value": return_value_to_log
|
|
141
|
+
"return_value": return_value_to_log,
|
|
142
|
+
"exec_source": execution_source_context.get()
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
if tracer.settings.global_custom_values:
|
|
@@ -121,3 +121,34 @@ def find_by_trace_id(trace_id: str, limit: int = 100) -> List[Dict[str, Any]]:
|
|
|
121
121
|
sort_ascending=True, # Sort chronologically
|
|
122
122
|
limit=limit
|
|
123
123
|
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def find_replay_executions(
|
|
127
|
+
limit: int = 20,
|
|
128
|
+
status: Optional[str] = None,
|
|
129
|
+
function_name: Optional[str] = None
|
|
130
|
+
) -> List[Dict[str, Any]]:
|
|
131
|
+
"""
|
|
132
|
+
Searches specifically for logs generated during a Replay session.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
limit: Max number of logs to retrieve.
|
|
136
|
+
status: Optional filter for 'SUCCESS' or 'ERROR'.
|
|
137
|
+
function_name: Optional filter for specific function name.
|
|
138
|
+
"""
|
|
139
|
+
logger.info(f"\n--- Searching for REPLAY Executions (Status: {status}, Func: {function_name}) ---")
|
|
140
|
+
|
|
141
|
+
filters = {"exec_source": "REPLAY"}
|
|
142
|
+
|
|
143
|
+
if status:
|
|
144
|
+
filters["status"] = status
|
|
145
|
+
|
|
146
|
+
if function_name:
|
|
147
|
+
filters["function_name"] = function_name
|
|
148
|
+
|
|
149
|
+
return find_executions(
|
|
150
|
+
filters=filters,
|
|
151
|
+
sort_by="timestamp_utc",
|
|
152
|
+
sort_ascending=False,
|
|
153
|
+
limit=limit
|
|
154
|
+
)
|
|
@@ -13,6 +13,7 @@ import weaviate.classes.query as wvc_query
|
|
|
13
13
|
from ..database.db import get_cached_client
|
|
14
14
|
from ..models.db_config import get_weaviate_settings
|
|
15
15
|
from ..monitoring.tracer import _mask_and_serialize
|
|
16
|
+
from .context import execution_source_context
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
18
19
|
|
|
@@ -78,7 +79,10 @@ class VectorWaveReplayer:
|
|
|
78
79
|
# [FIX] Extract only valid arguments for the target function
|
|
79
80
|
inputs = self._extract_inputs(raw_inputs, target_func)
|
|
80
81
|
|
|
82
|
+
token = None
|
|
81
83
|
try:
|
|
84
|
+
token = execution_source_context.set("REPLAY")
|
|
85
|
+
|
|
82
86
|
# 3. Function Re-execution
|
|
83
87
|
if is_async_func:
|
|
84
88
|
actual_output = asyncio.run(target_func(**inputs))
|
|
@@ -127,6 +131,10 @@ class VectorWaveReplayer:
|
|
|
127
131
|
"traceback": traceback.format_exc()
|
|
128
132
|
})
|
|
129
133
|
|
|
134
|
+
finally:
|
|
135
|
+
if token:
|
|
136
|
+
execution_source_context.reset(token)
|
|
137
|
+
|
|
130
138
|
logger.info(f"Replay Finished. Passed: {results['passed']}, Failed: {results['failed']}")
|
|
131
139
|
return results
|
|
132
140
|
|
|
@@ -6,6 +6,8 @@ import math
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import inspect
|
|
8
8
|
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from .context import execution_source_context
|
|
9
11
|
from ..core.llm.factory import get_llm_client
|
|
10
12
|
|
|
11
13
|
import weaviate.classes.query as wvc_query
|
|
@@ -88,12 +90,16 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
88
90
|
inputs = self._extract_inputs(raw_inputs, target_func)
|
|
89
91
|
|
|
90
92
|
try:
|
|
93
|
+
token = execution_source_context.set("REPLAY")
|
|
91
94
|
# 3. Function Re-execution
|
|
92
95
|
if is_async_func:
|
|
93
96
|
actual_output = asyncio.run(target_func(**inputs))
|
|
94
97
|
else:
|
|
95
98
|
actual_output = target_func(**inputs)
|
|
96
99
|
|
|
100
|
+
if token:
|
|
101
|
+
execution_source_context.reset(token)
|
|
102
|
+
|
|
97
103
|
# 4. Result Validation (Semantic Comparison)
|
|
98
104
|
is_match, match_reason = self._compare_results_semantic(
|
|
99
105
|
expected_output,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vectorwave
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: VectorWave: Seamless Auto-Vectorization Framework
|
|
5
5
|
Author-email: junyeonggim <junyeonggim5@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -618,6 +618,40 @@ archiver.export_and_clear(
|
|
|
618
618
|
{"messages": [{"role": "user", "content": "{\"price\": 100, \"tax\": 0.1}"}, {"role": "assistant", "content": "110.0"}]}
|
|
619
619
|
```
|
|
620
620
|
|
|
621
|
+
## 🌊 Auto-Injection (Zero-Code Change Integration)
|
|
622
|
+
|
|
623
|
+
You don't need to modify your business logic to use VectorWave. Use `VectorWaveAutoInjector` to inject functionality externally.
|
|
624
|
+
|
|
625
|
+
### How to use
|
|
626
|
+
|
|
627
|
+
1. **Configure Global Settings**: Set default values like `team`, `priority`, or `auto` (pending mode).
|
|
628
|
+
2. **Inject Modules**: Specify the target module path string.
|
|
629
|
+
|
|
630
|
+
```python
|
|
631
|
+
from vectorwave import initialize_database, VectorWaveAutoInjector, generate_and_register_metadata
|
|
632
|
+
|
|
633
|
+
# 1. Initialize DB
|
|
634
|
+
initialize_database()
|
|
635
|
+
|
|
636
|
+
# 2. Configure AutoInjector (Global Settings)
|
|
637
|
+
VectorWaveAutoInjector.configure(
|
|
638
|
+
team="billing-team",
|
|
639
|
+
priority=1,
|
|
640
|
+
auto=True # True: Collect metadata in memory (Pending), False: Save immediately to DB
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
# 3. Inject VectorWave into your module
|
|
644
|
+
# (No need to add @vectorize decorators in 'my_service.payment' code!)
|
|
645
|
+
VectorWaveAutoInjector.inject("my_service.payment")
|
|
646
|
+
|
|
647
|
+
# 4. Register Metadata (If auto=True)
|
|
648
|
+
generate_and_register_metadata()
|
|
649
|
+
|
|
650
|
+
# 5. Run your business logic
|
|
651
|
+
import my_service.payment
|
|
652
|
+
my_service.payment.process_transaction()
|
|
653
|
+
```
|
|
654
|
+
|
|
621
655
|
## 🤝 Contributing
|
|
622
656
|
|
|
623
657
|
We welcome all forms of contributions, including bug reports, feature requests, and code contributions. Please refer to [CONTRIBUTING.md](https://www.google.com/search?q=httpsS://www.google.com/search%3Fq%3DCONTRIBUTING.md) for details.
|
|
@@ -8,6 +8,8 @@ src/tests/conftest.py
|
|
|
8
8
|
src/tests/batch/__init__.py
|
|
9
9
|
src/tests/batch/test_batch.py
|
|
10
10
|
src/tests/core/__init__.py
|
|
11
|
+
src/tests/core/test_auto_injector.py
|
|
12
|
+
src/tests/core/test_auto_injector_real.py
|
|
11
13
|
src/tests/core/test_decorator.py
|
|
12
14
|
src/tests/core/test_semantic_caching.py
|
|
13
15
|
src/tests/core/llm/__init__.py
|
|
@@ -30,6 +32,7 @@ src/tests/search/__init__.py
|
|
|
30
32
|
src/tests/search/test_execution_search.py
|
|
31
33
|
src/tests/utils/__init__.py
|
|
32
34
|
src/tests/utils/test_function_cahe.py
|
|
35
|
+
src/tests/utils/test_replay_source.py
|
|
33
36
|
src/tests/utils/test_replayer.py
|
|
34
37
|
src/tests/utils/test_return_caching.py
|
|
35
38
|
src/tests/utils/test_status.py
|
|
@@ -43,6 +46,7 @@ src/vectorwave.egg-info/top_level.txt
|
|
|
43
46
|
src/vectorwave/batch/__init__.py
|
|
44
47
|
src/vectorwave/batch/batch.py
|
|
45
48
|
src/vectorwave/core/__init__.py
|
|
49
|
+
src/vectorwave/core/auto_injector.py
|
|
46
50
|
src/vectorwave/core/core.py
|
|
47
51
|
src/vectorwave/core/decorator.py
|
|
48
52
|
src/vectorwave/core/generator.py
|
|
@@ -74,6 +78,7 @@ src/vectorwave/search/execution_search.py
|
|
|
74
78
|
src/vectorwave/search/extended_search.py
|
|
75
79
|
src/vectorwave/search/rag_search.py
|
|
76
80
|
src/vectorwave/utils/__init__.py
|
|
81
|
+
src/vectorwave/utils/context.py
|
|
77
82
|
src/vectorwave/utils/function_cache.py
|
|
78
83
|
src/vectorwave/utils/healer.py
|
|
79
84
|
src/vectorwave/utils/replayer.py
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|