vectorwave 0.1.9__tar.gz → 0.2.1__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.1}/PKG-INFO +1 -1
- {vectorwave-0.1.9 → vectorwave-0.2.1}/pyproject.toml +1 -1
- vectorwave-0.2.1/src/tests/core/llm/test_token_usage.py +112 -0
- vectorwave-0.2.1/src/tests/utils/test_return_caching.py +126 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/__init__.py +7 -1
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/core/generator.py +11 -14
- vectorwave-0.2.1/src/vectorwave/core/llm/base.py +47 -0
- vectorwave-0.2.1/src/vectorwave/core/llm/factory.py +13 -0
- vectorwave-0.2.1/src/vectorwave/core/llm/openai_client.py +79 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/database/db.py +32 -1
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/database/db_search.py +84 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/search/rag_search.py +10 -17
- vectorwave-0.2.1/src/vectorwave/utils/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/healer.py +14 -14
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/replayer.py +38 -9
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/replayer_semantic.py +55 -44
- vectorwave-0.2.1/src/vectorwave/utils/return_caching_utils.py +134 -0
- vectorwave-0.2.1/src/vectorwave/vectorizer/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/vectorizer/openai_vectorizer.py +16 -6
- {vectorwave-0.1.9 → vectorwave-0.2.1/src/vectorwave.egg-info}/PKG-INFO +1 -1
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave.egg-info/SOURCES.txt +7 -0
- vectorwave-0.1.9/src/vectorwave/utils/return_caching_utils.py +0 -76
- {vectorwave-0.1.9 → vectorwave-0.2.1}/LICENSE +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/MANIFEST.in +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/NOTICE +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/Readme.md +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/setup.cfg +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/batch/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/batch/test_batch.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/conftest.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/core/__init__.py +0 -0
- {vectorwave-0.1.9/src/tests/database → vectorwave-0.2.1/src/tests/core/llm}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/core/test_decorator.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/core/test_semantic_caching.py +0 -0
- {vectorwave-0.1.9/src/tests/exception → vectorwave-0.2.1/src/tests/database}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/database/test_archiver.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/database/test_db.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/database/test_db_search.py +0 -0
- {vectorwave-0.1.9/src/tests/models → vectorwave-0.2.1/src/tests/exception}/__init__.py +0 -0
- {vectorwave-0.1.9/src/tests/monitoring → vectorwave-0.2.1/src/tests/models}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/models/test_db_config.py +0 -0
- {vectorwave-0.1.9/src/tests/monitoring/alert → vectorwave-0.2.1/src/tests/monitoring}/__init__.py +0 -0
- {vectorwave-0.1.9/src/tests/prediction → vectorwave-0.2.1/src/tests/monitoring/alert}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/monitoring/alert/test_alerter.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/monitoring/test_async_trace.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/monitoring/test_tracer.py +0 -0
- {vectorwave-0.1.9/src/tests/search → vectorwave-0.2.1/src/tests/prediction}/__init__.py +0 -0
- {vectorwave-0.1.9/src/tests/utils → vectorwave-0.2.1/src/tests/search}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/search/test_execution_search.py +0 -0
- {vectorwave-0.1.9/src/tests/vectorizer → vectorwave-0.2.1/src/tests/utils}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/utils/test_function_cahe.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/utils/test_replayer.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/utils/test_status.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/batch → vectorwave-0.2.1/src/tests/vectorizer}/__init__.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/core → vectorwave-0.2.1/src/vectorwave/batch}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/batch/batch.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/database → vectorwave-0.2.1/src/vectorwave/core}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/core/core.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/core/decorator.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/exception → vectorwave-0.2.1/src/vectorwave/core/llm}/__init__.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/models → vectorwave-0.2.1/src/vectorwave/database}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/database/archiver.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/monitoring → vectorwave-0.2.1/src/vectorwave/exception}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/exception/exceptions.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/monitoring/alert → vectorwave-0.2.1/src/vectorwave/models}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/models/db_config.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/prediction → vectorwave-0.2.1/src/vectorwave/monitoring}/__init__.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/search → vectorwave-0.2.1/src/vectorwave/monitoring/alert}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/base.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/factory.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/null_alerter.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/webhook_alerter.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/monitoring.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/tracer.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/utils → vectorwave-0.2.1/src/vectorwave/prediction}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/prediction/predictor.py +0 -0
- {vectorwave-0.1.9/src/vectorwave/vectorizer → vectorwave-0.2.1/src/vectorwave/search}/__init__.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/search/execution_search.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/search/extended_search.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/function_cache.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/status.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/vectorizer/base.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/vectorizer/factory.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/vectorizer/huggingface_vectorizer.py +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave.egg-info/dependency_links.txt +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave.egg-info/requires.txt +0 -0
- {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
from vectorwave.core.llm.openai_client import VectorWaveOpenAIClient
|
|
4
|
+
|
|
5
|
+
@pytest.fixture
|
|
6
|
+
def mock_deps(monkeypatch):
|
|
7
|
+
"""
|
|
8
|
+
Mocks the dependencies (BatchManager, OpenAI, Settings) of VectorWaveOpenAIClient.
|
|
9
|
+
"""
|
|
10
|
+
# 1. Mock BatchManager (Target for verifying token usage persistence)
|
|
11
|
+
mock_batch = MagicMock()
|
|
12
|
+
mock_batch.add_object = MagicMock()
|
|
13
|
+
mock_get_batch = MagicMock(return_value=mock_batch)
|
|
14
|
+
monkeypatch.setattr("vectorwave.core.llm.openai_client.get_batch_manager", mock_get_batch)
|
|
15
|
+
|
|
16
|
+
# 2. Mock Settings (Bypasses API Key check)
|
|
17
|
+
mock_settings = MagicMock()
|
|
18
|
+
mock_settings.OPENAI_API_KEY = "sk-test-key"
|
|
19
|
+
mock_get_settings = MagicMock(return_value=mock_settings)
|
|
20
|
+
monkeypatch.setattr("vectorwave.core.llm.openai_client.get_weaviate_settings", mock_get_settings)
|
|
21
|
+
|
|
22
|
+
# 3. Mock OpenAI Class (Bypasses actual API calls)
|
|
23
|
+
mock_openai_cls = MagicMock()
|
|
24
|
+
monkeypatch.setattr("vectorwave.core.llm.openai_client.OpenAI", mock_openai_cls)
|
|
25
|
+
|
|
26
|
+
# Note: Clearing the lru_cache for the singleton instance is typically
|
|
27
|
+
# needed if the client is imported globally. We assume the current test
|
|
28
|
+
# setup allows for fresh initialization here.
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
"batch": mock_batch,
|
|
32
|
+
"openai_cls": mock_openai_cls
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def test_chat_completion_logs_token_usage(mock_deps):
|
|
36
|
+
"""
|
|
37
|
+
[Case 1] Verify that token usage is logged as 'generation' type during chat completion.
|
|
38
|
+
"""
|
|
39
|
+
# Arrange
|
|
40
|
+
# Get the mock client instance returned when OpenAI() is called
|
|
41
|
+
mock_client_instance = mock_deps["openai_cls"].return_value
|
|
42
|
+
|
|
43
|
+
# Mock OpenAI Response structure
|
|
44
|
+
mock_response = MagicMock()
|
|
45
|
+
mock_response.choices = [MagicMock(message=MagicMock(content="Test response"))]
|
|
46
|
+
mock_response.usage.total_tokens = 123 # Token count to test
|
|
47
|
+
mock_client_instance.chat.completions.create.return_value = mock_response
|
|
48
|
+
|
|
49
|
+
# Act
|
|
50
|
+
# Create a new client instance for the test
|
|
51
|
+
client = VectorWaveOpenAIClient()
|
|
52
|
+
result = client.create_chat_completion(
|
|
53
|
+
messages=[{"role": "user", "content": "Hi"}],
|
|
54
|
+
model="gpt-4-test",
|
|
55
|
+
category="test_chat_category"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Assert
|
|
59
|
+
# 1. Verify that the function returns the expected result
|
|
60
|
+
assert result == "Test response"
|
|
61
|
+
|
|
62
|
+
# 2. Verify that BatchManager.add_object was called (log saved)
|
|
63
|
+
mock_batch = mock_deps["batch"]
|
|
64
|
+
mock_batch.add_object.assert_called_once()
|
|
65
|
+
|
|
66
|
+
# 3. Verify the properties of the saved log entry
|
|
67
|
+
call_kwargs = mock_batch.add_object.call_args.kwargs
|
|
68
|
+
props = call_kwargs["properties"]
|
|
69
|
+
|
|
70
|
+
assert call_kwargs["collection"] == "VectorWaveTokenUsage"
|
|
71
|
+
assert props["tokens"] == 123
|
|
72
|
+
assert props["model"] == "gpt-4-test"
|
|
73
|
+
assert props["category"] == "test_chat_category"
|
|
74
|
+
assert props["usage_type"] == "generation"
|
|
75
|
+
|
|
76
|
+
def test_create_embedding_logs_token_usage(mock_deps):
|
|
77
|
+
"""
|
|
78
|
+
[Case 2] Verify that token usage is logged as 'embedding' type during embedding creation.
|
|
79
|
+
"""
|
|
80
|
+
# Arrange
|
|
81
|
+
mock_client_instance = mock_deps["openai_cls"].return_value
|
|
82
|
+
|
|
83
|
+
# Mock OpenAI Response structure
|
|
84
|
+
mock_response = MagicMock()
|
|
85
|
+
mock_response.data = [MagicMock(embedding=[0.1, 0.2, 0.3])]
|
|
86
|
+
mock_response.usage.total_tokens = 45 # Token count to test
|
|
87
|
+
mock_client_instance.embeddings.create.return_value = mock_response
|
|
88
|
+
|
|
89
|
+
# Act
|
|
90
|
+
client = VectorWaveOpenAIClient()
|
|
91
|
+
result = client.create_embedding(
|
|
92
|
+
text="Test text",
|
|
93
|
+
model="text-embedding-3-small",
|
|
94
|
+
category="test_embed_category"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Assert
|
|
98
|
+
# 1. Verify that the function returns the expected result
|
|
99
|
+
assert result == [0.1, 0.2, 0.3]
|
|
100
|
+
|
|
101
|
+
# 2. Verify that BatchManager.add_object was called
|
|
102
|
+
mock_batch = mock_deps["batch"]
|
|
103
|
+
mock_batch.add_object.assert_called_once()
|
|
104
|
+
|
|
105
|
+
# 3. Verify the properties of the saved log entry
|
|
106
|
+
call_kwargs = mock_batch.add_object.call_args.kwargs
|
|
107
|
+
props = call_kwargs["properties"]
|
|
108
|
+
|
|
109
|
+
assert call_kwargs["collection"] == "VectorWaveTokenUsage"
|
|
110
|
+
assert props["tokens"] == 45
|
|
111
|
+
assert props["usage_type"] == "embedding"
|
|
112
|
+
assert props["category"] == "test_embed_category"
|
|
@@ -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
|
]
|
|
@@ -7,6 +7,7 @@ from ..models.db_config import get_weaviate_settings
|
|
|
7
7
|
from ..batch.batch import get_batch_manager
|
|
8
8
|
from ..vectorizer.factory import get_vectorizer
|
|
9
9
|
from .decorator import PENDING_FUNCTIONS
|
|
10
|
+
from .llm.factory import get_llm_client
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
@@ -15,20 +16,11 @@ try:
|
|
|
15
16
|
except ImportError:
|
|
16
17
|
OpenAI = None
|
|
17
18
|
|
|
18
|
-
def _get_openai_client(settings):
|
|
19
|
-
if OpenAI is None:
|
|
20
|
-
logger.error("OpenAI library not installed. Install with 'pip install openai'.")
|
|
21
|
-
return None
|
|
22
|
-
if not settings.OPENAI_API_KEY:
|
|
23
|
-
logger.error("OPENAI_API_KEY is missing in .env.")
|
|
24
|
-
return None
|
|
25
|
-
return OpenAI(api_key=settings.OPENAI_API_KEY)
|
|
26
|
-
|
|
27
19
|
|
|
28
20
|
def generate_metadata_via_llm(source_code: str, func_name: str) -> Optional[Dict[str, str]]:
|
|
29
21
|
"""Call LLM to generate description and narrative from source code."""
|
|
30
22
|
settings = get_weaviate_settings()
|
|
31
|
-
client =
|
|
23
|
+
client = get_llm_client()
|
|
32
24
|
if not client:
|
|
33
25
|
return None
|
|
34
26
|
|
|
@@ -47,17 +39,22 @@ def generate_metadata_via_llm(source_code: str, func_name: str) -> Optional[Dict
|
|
|
47
39
|
"""
|
|
48
40
|
|
|
49
41
|
try:
|
|
50
|
-
|
|
42
|
+
# Refactored to use BaseLLMClient interface
|
|
43
|
+
response_text = client.create_chat_completion(
|
|
51
44
|
model="gpt-4o-mini",
|
|
52
45
|
messages=[
|
|
53
46
|
{"role": "system", "content": "You are a technical documentation assistant. Output only JSON."},
|
|
54
47
|
{"role": "user", "content": prompt}
|
|
55
48
|
],
|
|
56
49
|
temperature=0.0,
|
|
57
|
-
response_format={"type": "json_object"}
|
|
50
|
+
response_format={"type": "json_object"},
|
|
51
|
+
category="auto_doc"
|
|
58
52
|
)
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
|
|
54
|
+
if response_text:
|
|
55
|
+
return json.loads(response_text)
|
|
56
|
+
return None
|
|
57
|
+
|
|
61
58
|
except Exception as e:
|
|
62
59
|
logger.error(f"LLM generation failed for '{func_name}': {e}")
|
|
63
60
|
return None
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List, Dict, Optional
|
|
3
|
+
|
|
4
|
+
class BaseLLMClient(ABC):
|
|
5
|
+
"""
|
|
6
|
+
Abstract interface that all LLM Providers (OpenAI, Anthropic, etc.) must implement.
|
|
7
|
+
Implementations must handle internal token usage logging.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def create_embedding(self, text: str, model: str, category: str = "default") -> Optional[List[float]]:
|
|
12
|
+
"""
|
|
13
|
+
Generates text embeddings.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
text: The text to embed.
|
|
17
|
+
model: The name of the model to use.
|
|
18
|
+
category: Category for aggregating token usage (e.g., 'execution_log', 'auto_doc').
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The generated list of embedding vectors (None on failure).
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def create_chat_completion(
|
|
27
|
+
self,
|
|
28
|
+
messages: List[Dict],
|
|
29
|
+
model: str,
|
|
30
|
+
temperature: float = 0.1,
|
|
31
|
+
response_format: Optional[Dict] = None,
|
|
32
|
+
category: str = "default"
|
|
33
|
+
) -> Optional[str]:
|
|
34
|
+
"""
|
|
35
|
+
Generates a chat completion (response).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
messages: List of conversation messages [{"role": "user", "content": "..."}].
|
|
39
|
+
model: The name of the model to use.
|
|
40
|
+
temperature: Parameter for controlling generation diversity.
|
|
41
|
+
response_format: Response format (e.g., {"type": "json_object"}).
|
|
42
|
+
category: Category for aggregating token usage (e.g., 'execution_log', 'auto_doc').
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The generated text response (None on failure).
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .base import BaseLLMClient
|
|
2
|
+
from .openai_client import VectorWaveOpenAIClient
|
|
3
|
+
|
|
4
|
+
_llm_instance: BaseLLMClient = None
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_llm_client() -> BaseLLMClient:
|
|
8
|
+
"""Returns the singleton LLM client instance."""
|
|
9
|
+
global _llm_instance
|
|
10
|
+
if _llm_instance is None:
|
|
11
|
+
# Can be extended later to return different clients (Anthropic, etc.) based on settings (VECTORIZER, etc.)
|
|
12
|
+
_llm_instance = VectorWaveOpenAIClient()
|
|
13
|
+
return _llm_instance
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# src/vectorwave/core/llm/openai_client.py
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Dict, Optional
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from ...models.db_config import get_weaviate_settings
|
|
6
|
+
from ...batch.batch import get_batch_manager # [추가]
|
|
7
|
+
from .base import BaseLLMClient
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from openai import OpenAI
|
|
13
|
+
except ImportError:
|
|
14
|
+
OpenAI = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VectorWaveOpenAIClient(BaseLLMClient):
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.settings = get_weaviate_settings()
|
|
20
|
+
self.batch_manager = get_batch_manager()
|
|
21
|
+
|
|
22
|
+
if OpenAI is None or not self.settings.OPENAI_API_KEY:
|
|
23
|
+
self.client = None
|
|
24
|
+
else:
|
|
25
|
+
self.client = OpenAI(api_key=self.settings.OPENAI_API_KEY)
|
|
26
|
+
|
|
27
|
+
def _log_usage(self, tokens: int, model: str, usage_type: str, category: str):
|
|
28
|
+
if tokens > 0:
|
|
29
|
+
try:
|
|
30
|
+
self.batch_manager.add_object(
|
|
31
|
+
collection="VectorWaveTokenUsage",
|
|
32
|
+
properties={
|
|
33
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
34
|
+
"model": model,
|
|
35
|
+
"usage_type": usage_type,
|
|
36
|
+
"category": category,
|
|
37
|
+
"tokens": tokens
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.warning(f"Failed to log token usage: {e}")
|
|
42
|
+
|
|
43
|
+
def create_embedding(self, text: str, model: str = "text-embedding-3-small", category: str = "default") -> Optional[
|
|
44
|
+
List[float]]:
|
|
45
|
+
"""
|
|
46
|
+
Returns: Vector list only (Tokens are logged internally)
|
|
47
|
+
"""
|
|
48
|
+
if not self.client: return None
|
|
49
|
+
try:
|
|
50
|
+
text = text.replace("\n", " ")
|
|
51
|
+
res = self.client.embeddings.create(input=[text], model=model)
|
|
52
|
+
|
|
53
|
+
tokens = res.usage.total_tokens if res.usage else 0
|
|
54
|
+
self._log_usage(tokens, model, "embedding", category)
|
|
55
|
+
|
|
56
|
+
return res.data[0].embedding
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.error(f"Embedding error: {e}")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def create_chat_completion(self, messages: List[Dict], model: str = "gpt-4-turbo", temperature: float = 0.1,
|
|
62
|
+
response_format=None, category: str = "default") -> Optional[str]:
|
|
63
|
+
"""
|
|
64
|
+
Returns: Content string only (Tokens are logged internally)
|
|
65
|
+
"""
|
|
66
|
+
if not self.client: return None
|
|
67
|
+
try:
|
|
68
|
+
kwargs = {"model": model, "messages": messages, "temperature": temperature}
|
|
69
|
+
if response_format: kwargs["response_format"] = response_format
|
|
70
|
+
|
|
71
|
+
res = self.client.chat.completions.create(**kwargs)
|
|
72
|
+
|
|
73
|
+
tokens = res.usage.total_tokens if res.usage else 0
|
|
74
|
+
self._log_usage(tokens, model, "generation", category)
|
|
75
|
+
|
|
76
|
+
return res.choices[0].message.content
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Completion error: {e}")
|
|
79
|
+
return None
|
|
@@ -16,6 +16,7 @@ from weaviate.exceptions import WeaviateConnectionError as WeaviateClientConnect
|
|
|
16
16
|
# Create module-level logger
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
# Code based on Weaviate v4 (latest) client.
|
|
20
21
|
|
|
21
22
|
def get_weaviate_client(settings: WeaviateSettings) -> weaviate.WeaviateClient:
|
|
@@ -324,6 +325,36 @@ def create_execution_schema(client: weaviate.WeaviateClient, settings: WeaviateS
|
|
|
324
325
|
raise SchemaCreationError(f"Error during execution schema creation: {e}")
|
|
325
326
|
|
|
326
327
|
|
|
328
|
+
def create_usage_schema(client: weaviate.WeaviateClient, settings: WeaviateSettings):
|
|
329
|
+
"""
|
|
330
|
+
API call token analysis schema
|
|
331
|
+
"""
|
|
332
|
+
collection_name = "VectorWaveTokenUsage"
|
|
333
|
+
|
|
334
|
+
if client.collections.exists(collection_name):
|
|
335
|
+
return client.collections.get(collection_name)
|
|
336
|
+
|
|
337
|
+
logger.info("Creating collection '%s'", collection_name)
|
|
338
|
+
|
|
339
|
+
properties = [
|
|
340
|
+
wvc.Property(name="timestamp_utc", data_type=wvc.DataType.DATE),
|
|
341
|
+
wvc.Property(name="model", data_type=wvc.DataType.TEXT),
|
|
342
|
+
wvc.Property(name="usage_type", data_type=wvc.DataType.TEXT), # "embedding", "generation" 등
|
|
343
|
+
wvc.Property(name="category", data_type=wvc.DataType.TEXT), # "execution_log", "auto_doc" 등
|
|
344
|
+
wvc.Property(name="tokens", data_type=wvc.DataType.INT),
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
return client.collections.create(
|
|
349
|
+
name=collection_name,
|
|
350
|
+
properties=properties,
|
|
351
|
+
vectorizer_config=wvc.Configure.Vectorizer.none(),
|
|
352
|
+
)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.error(f"Error creating usage schema: {e}")
|
|
355
|
+
raise SchemaCreationError(f"Error during usage schema creation: {e}")
|
|
356
|
+
|
|
357
|
+
|
|
327
358
|
def initialize_database():
|
|
328
359
|
"""
|
|
329
360
|
Helper function to initialize both the client and the two schemas.
|
|
@@ -334,8 +365,8 @@ def initialize_database():
|
|
|
334
365
|
if client:
|
|
335
366
|
create_vectorwave_schema(client, settings)
|
|
336
367
|
create_execution_schema(client, settings)
|
|
368
|
+
create_usage_schema(client, settings)
|
|
337
369
|
return client
|
|
338
370
|
except Exception as e:
|
|
339
371
|
logger.error("Failed to initialize VectorWave database: %s", e)
|
|
340
372
|
return None
|
|
341
|
-
|
|
@@ -10,6 +10,7 @@ from ..models.db_config import get_weaviate_settings, WeaviateSettings
|
|
|
10
10
|
from .db import get_cached_client
|
|
11
11
|
from ..exception.exceptions import WeaviateConnectionError
|
|
12
12
|
from ..vectorizer.factory import get_vectorizer
|
|
13
|
+
from weaviate.classes.aggregate import Metrics
|
|
13
14
|
|
|
14
15
|
import uuid
|
|
15
16
|
from datetime import datetime
|
|
@@ -409,3 +410,86 @@ def check_semantic_drift(
|
|
|
409
410
|
except Exception as e:
|
|
410
411
|
logger.error(f"Failed to check semantic drift: {e}")
|
|
411
412
|
return False, 0.0, None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def simulate_drift_check(
|
|
416
|
+
text: str,
|
|
417
|
+
function_name: str,
|
|
418
|
+
threshold: Optional[float] = None,
|
|
419
|
+
k: Optional[int] = None
|
|
420
|
+
) -> Dict[str, Any]:
|
|
421
|
+
"""
|
|
422
|
+
Simulates drift detection for a hypothetical input string without executing the function.
|
|
423
|
+
Useful for 'Drift Radar' or debugging.
|
|
424
|
+
"""
|
|
425
|
+
try:
|
|
426
|
+
settings = get_weaviate_settings()
|
|
427
|
+
vectorizer = get_vectorizer()
|
|
428
|
+
|
|
429
|
+
if not vectorizer:
|
|
430
|
+
return {"error": "No vectorizer configured."}
|
|
431
|
+
|
|
432
|
+
# 1. Set defaults from settings if not provided
|
|
433
|
+
if threshold is None:
|
|
434
|
+
threshold = settings.DRIFT_DISTANCE_THRESHOLD
|
|
435
|
+
if k is None:
|
|
436
|
+
k = settings.DRIFT_NEIGHBOR_AMOUNT
|
|
437
|
+
|
|
438
|
+
# 2. Vectorize the input text
|
|
439
|
+
try:
|
|
440
|
+
vector = vectorizer.embed(text)
|
|
441
|
+
except Exception as e:
|
|
442
|
+
return {"error": f"Vectorization failed: {e}"}
|
|
443
|
+
|
|
444
|
+
# 3. Perform the check using the existing logic
|
|
445
|
+
is_drift, avg_distance, nearest_uuid = check_semantic_drift(
|
|
446
|
+
vector=vector,
|
|
447
|
+
function_name=function_name,
|
|
448
|
+
threshold=threshold,
|
|
449
|
+
k=k
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
"function_name": function_name,
|
|
454
|
+
"input_text": text,
|
|
455
|
+
"is_drift": is_drift,
|
|
456
|
+
"avg_distance": avg_distance,
|
|
457
|
+
"threshold": threshold,
|
|
458
|
+
"nearest_neighbor_uuid": nearest_uuid,
|
|
459
|
+
"status": "ANOMALY" if is_drift else "NORMAL"
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
except Exception as e:
|
|
463
|
+
logger.error(f"Simulation failed: {e}")
|
|
464
|
+
return {"error": str(e)}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def get_token_usage_stats() -> Dict[str, int]:
|
|
468
|
+
"""VectorWaveTokenUsage collections based analysis"""
|
|
469
|
+
try:
|
|
470
|
+
client = get_cached_client()
|
|
471
|
+
if not client.collections.exists("VectorWaveTokenUsage"):
|
|
472
|
+
logger.warning("VectorWaveTokenUsage collection does not exist.")
|
|
473
|
+
return {"total_tokens": 0}
|
|
474
|
+
|
|
475
|
+
usage_col = client.collections.get("VectorWaveTokenUsage")
|
|
476
|
+
|
|
477
|
+
total_tokens = 0
|
|
478
|
+
stats = {}
|
|
479
|
+
|
|
480
|
+
for obj in usage_col.iterator():
|
|
481
|
+
props = obj.properties
|
|
482
|
+
tokens = int(props.get("tokens", 0))
|
|
483
|
+
category = props.get("category", "unknown")
|
|
484
|
+
|
|
485
|
+
total_tokens += tokens
|
|
486
|
+
|
|
487
|
+
cat_key = f"{category}_tokens"
|
|
488
|
+
stats[cat_key] = stats.get(cat_key, 0) + tokens
|
|
489
|
+
|
|
490
|
+
stats["total_tokens"] = total_tokens
|
|
491
|
+
return stats
|
|
492
|
+
|
|
493
|
+
except Exception as e:
|
|
494
|
+
logger.error(f"Stats error: {e}", exc_info=True)
|
|
495
|
+
return {}
|