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.
Files changed (88) hide show
  1. {vectorwave-0.1.9/src/vectorwave.egg-info → vectorwave-0.2.1}/PKG-INFO +1 -1
  2. {vectorwave-0.1.9 → vectorwave-0.2.1}/pyproject.toml +1 -1
  3. vectorwave-0.2.1/src/tests/core/llm/test_token_usage.py +112 -0
  4. vectorwave-0.2.1/src/tests/utils/test_return_caching.py +126 -0
  5. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/__init__.py +7 -1
  6. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/core/generator.py +11 -14
  7. vectorwave-0.2.1/src/vectorwave/core/llm/base.py +47 -0
  8. vectorwave-0.2.1/src/vectorwave/core/llm/factory.py +13 -0
  9. vectorwave-0.2.1/src/vectorwave/core/llm/openai_client.py +79 -0
  10. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/database/db.py +32 -1
  11. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/database/db_search.py +84 -0
  12. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/search/rag_search.py +10 -17
  13. vectorwave-0.2.1/src/vectorwave/utils/__init__.py +0 -0
  14. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/healer.py +14 -14
  15. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/replayer.py +38 -9
  16. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/replayer_semantic.py +55 -44
  17. vectorwave-0.2.1/src/vectorwave/utils/return_caching_utils.py +134 -0
  18. vectorwave-0.2.1/src/vectorwave/vectorizer/__init__.py +0 -0
  19. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/vectorizer/openai_vectorizer.py +16 -6
  20. {vectorwave-0.1.9 → vectorwave-0.2.1/src/vectorwave.egg-info}/PKG-INFO +1 -1
  21. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave.egg-info/SOURCES.txt +7 -0
  22. vectorwave-0.1.9/src/vectorwave/utils/return_caching_utils.py +0 -76
  23. {vectorwave-0.1.9 → vectorwave-0.2.1}/LICENSE +0 -0
  24. {vectorwave-0.1.9 → vectorwave-0.2.1}/MANIFEST.in +0 -0
  25. {vectorwave-0.1.9 → vectorwave-0.2.1}/NOTICE +0 -0
  26. {vectorwave-0.1.9 → vectorwave-0.2.1}/Readme.md +0 -0
  27. {vectorwave-0.1.9 → vectorwave-0.2.1}/setup.cfg +0 -0
  28. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/__init__.py +0 -0
  29. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/batch/__init__.py +0 -0
  30. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/batch/test_batch.py +0 -0
  31. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/conftest.py +0 -0
  32. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/core/__init__.py +0 -0
  33. {vectorwave-0.1.9/src/tests/database → vectorwave-0.2.1/src/tests/core/llm}/__init__.py +0 -0
  34. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/core/test_decorator.py +0 -0
  35. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/core/test_semantic_caching.py +0 -0
  36. {vectorwave-0.1.9/src/tests/exception → vectorwave-0.2.1/src/tests/database}/__init__.py +0 -0
  37. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/database/test_archiver.py +0 -0
  38. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/database/test_db.py +0 -0
  39. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/database/test_db_search.py +0 -0
  40. {vectorwave-0.1.9/src/tests/models → vectorwave-0.2.1/src/tests/exception}/__init__.py +0 -0
  41. {vectorwave-0.1.9/src/tests/monitoring → vectorwave-0.2.1/src/tests/models}/__init__.py +0 -0
  42. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/models/test_db_config.py +0 -0
  43. {vectorwave-0.1.9/src/tests/monitoring/alert → vectorwave-0.2.1/src/tests/monitoring}/__init__.py +0 -0
  44. {vectorwave-0.1.9/src/tests/prediction → vectorwave-0.2.1/src/tests/monitoring/alert}/__init__.py +0 -0
  45. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/monitoring/alert/test_alerter.py +0 -0
  46. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/monitoring/test_async_trace.py +0 -0
  47. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/monitoring/test_tracer.py +0 -0
  48. {vectorwave-0.1.9/src/tests/search → vectorwave-0.2.1/src/tests/prediction}/__init__.py +0 -0
  49. {vectorwave-0.1.9/src/tests/utils → vectorwave-0.2.1/src/tests/search}/__init__.py +0 -0
  50. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/search/test_execution_search.py +0 -0
  51. {vectorwave-0.1.9/src/tests/vectorizer → vectorwave-0.2.1/src/tests/utils}/__init__.py +0 -0
  52. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/utils/test_function_cahe.py +0 -0
  53. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/utils/test_replayer.py +0 -0
  54. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/tests/utils/test_status.py +0 -0
  55. {vectorwave-0.1.9/src/vectorwave/batch → vectorwave-0.2.1/src/tests/vectorizer}/__init__.py +0 -0
  56. {vectorwave-0.1.9/src/vectorwave/core → vectorwave-0.2.1/src/vectorwave/batch}/__init__.py +0 -0
  57. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/batch/batch.py +0 -0
  58. {vectorwave-0.1.9/src/vectorwave/database → vectorwave-0.2.1/src/vectorwave/core}/__init__.py +0 -0
  59. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/core/core.py +0 -0
  60. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/core/decorator.py +0 -0
  61. {vectorwave-0.1.9/src/vectorwave/exception → vectorwave-0.2.1/src/vectorwave/core/llm}/__init__.py +0 -0
  62. {vectorwave-0.1.9/src/vectorwave/models → vectorwave-0.2.1/src/vectorwave/database}/__init__.py +0 -0
  63. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/database/archiver.py +0 -0
  64. {vectorwave-0.1.9/src/vectorwave/monitoring → vectorwave-0.2.1/src/vectorwave/exception}/__init__.py +0 -0
  65. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/exception/exceptions.py +0 -0
  66. {vectorwave-0.1.9/src/vectorwave/monitoring/alert → vectorwave-0.2.1/src/vectorwave/models}/__init__.py +0 -0
  67. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/models/db_config.py +0 -0
  68. {vectorwave-0.1.9/src/vectorwave/prediction → vectorwave-0.2.1/src/vectorwave/monitoring}/__init__.py +0 -0
  69. {vectorwave-0.1.9/src/vectorwave/search → vectorwave-0.2.1/src/vectorwave/monitoring/alert}/__init__.py +0 -0
  70. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/base.py +0 -0
  71. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/factory.py +0 -0
  72. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/null_alerter.py +0 -0
  73. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/webhook_alerter.py +0 -0
  74. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/monitoring.py +0 -0
  75. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/monitoring/tracer.py +0 -0
  76. {vectorwave-0.1.9/src/vectorwave/utils → vectorwave-0.2.1/src/vectorwave/prediction}/__init__.py +0 -0
  77. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/prediction/predictor.py +0 -0
  78. {vectorwave-0.1.9/src/vectorwave/vectorizer → vectorwave-0.2.1/src/vectorwave/search}/__init__.py +0 -0
  79. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/search/execution_search.py +0 -0
  80. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/search/extended_search.py +0 -0
  81. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/function_cache.py +0 -0
  82. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/utils/status.py +0 -0
  83. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/vectorizer/base.py +0 -0
  84. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/vectorizer/factory.py +0 -0
  85. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave/vectorizer/huggingface_vectorizer.py +0 -0
  86. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave.egg-info/dependency_links.txt +0 -0
  87. {vectorwave-0.1.9 → vectorwave-0.2.1}/src/vectorwave.egg-info/requires.txt +0 -0
  88. {vectorwave-0.1.9 → vectorwave-0.2.1}/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.1.9
3
+ Version: 0.2.1
4
4
  Summary: VectorWave: Seamless Auto-Vectorization Framework
5
5
  Author-email: junyeonggim <junyeonggim5@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vectorwave"
7
- version = "0.1.9"
7
+ version = "0.2.01"
8
8
  authors = [
9
9
  { name = "junyeonggim", email = "junyeonggim5@gmail.com" },
10
10
  ]
@@ -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 = _get_openai_client(settings)
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
- response = client.chat.completions.create(
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
- content = response.choices[0].message.content
60
- return json.loads(content)
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 {}