vectorwave 0.2.0__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.2.0/src/vectorwave.egg-info → vectorwave-0.2.1}/PKG-INFO +1 -1
- {vectorwave-0.2.0 → 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.0 → 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.2.0 → vectorwave-0.2.1}/src/vectorwave/database/db.py +32 -1
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/database/db_search.py +32 -0
- {vectorwave-0.2.0 → 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.2.0 → vectorwave-0.2.1}/src/vectorwave/utils/healer.py +14 -14
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/utils/replayer_semantic.py +16 -13
- vectorwave-0.2.1/src/vectorwave/vectorizer/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/vectorizer/openai_vectorizer.py +16 -6
- {vectorwave-0.2.0 → vectorwave-0.2.1/src/vectorwave.egg-info}/PKG-INFO +1 -1
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave.egg-info/SOURCES.txt +6 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/LICENSE +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/MANIFEST.in +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/NOTICE +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/Readme.md +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/setup.cfg +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/batch/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/batch/test_batch.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/conftest.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/core/__init__.py +0 -0
- {vectorwave-0.2.0/src/tests/database → vectorwave-0.2.1/src/tests/core/llm}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/core/test_decorator.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/core/test_semantic_caching.py +0 -0
- {vectorwave-0.2.0/src/tests/exception → vectorwave-0.2.1/src/tests/database}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/database/test_archiver.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/database/test_db.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/database/test_db_search.py +0 -0
- {vectorwave-0.2.0/src/tests/models → vectorwave-0.2.1/src/tests/exception}/__init__.py +0 -0
- {vectorwave-0.2.0/src/tests/monitoring → vectorwave-0.2.1/src/tests/models}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/models/test_db_config.py +0 -0
- {vectorwave-0.2.0/src/tests/monitoring/alert → vectorwave-0.2.1/src/tests/monitoring}/__init__.py +0 -0
- {vectorwave-0.2.0/src/tests/prediction → vectorwave-0.2.1/src/tests/monitoring/alert}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/monitoring/alert/test_alerter.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/monitoring/test_async_trace.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/monitoring/test_tracer.py +0 -0
- {vectorwave-0.2.0/src/tests/search → vectorwave-0.2.1/src/tests/prediction}/__init__.py +0 -0
- {vectorwave-0.2.0/src/tests/utils → vectorwave-0.2.1/src/tests/search}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/search/test_execution_search.py +0 -0
- {vectorwave-0.2.0/src/tests/vectorizer → vectorwave-0.2.1/src/tests/utils}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/utils/test_function_cahe.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/utils/test_replayer.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/utils/test_return_caching.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/tests/utils/test_status.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/batch → vectorwave-0.2.1/src/tests/vectorizer}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/__init__.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/core → vectorwave-0.2.1/src/vectorwave/batch}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/batch/batch.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/database → vectorwave-0.2.1/src/vectorwave/core}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/core/core.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/core/decorator.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/exception → vectorwave-0.2.1/src/vectorwave/core/llm}/__init__.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/models → vectorwave-0.2.1/src/vectorwave/database}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/database/archiver.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/monitoring → vectorwave-0.2.1/src/vectorwave/exception}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/exception/exceptions.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/monitoring/alert → vectorwave-0.2.1/src/vectorwave/models}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/models/db_config.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/prediction → vectorwave-0.2.1/src/vectorwave/monitoring}/__init__.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/search → vectorwave-0.2.1/src/vectorwave/monitoring/alert}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/base.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/factory.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/null_alerter.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/monitoring/alert/webhook_alerter.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/monitoring/monitoring.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/monitoring/tracer.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/utils → vectorwave-0.2.1/src/vectorwave/prediction}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/prediction/predictor.py +0 -0
- {vectorwave-0.2.0/src/vectorwave/vectorizer → vectorwave-0.2.1/src/vectorwave/search}/__init__.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/search/execution_search.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/search/extended_search.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/utils/function_cache.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/utils/replayer.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/utils/return_caching_utils.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/utils/status.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/vectorizer/base.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/vectorizer/factory.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave/vectorizer/huggingface_vectorizer.py +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave.egg-info/dependency_links.txt +0 -0
- {vectorwave-0.2.0 → vectorwave-0.2.1}/src/vectorwave.egg-info/requires.txt +0 -0
- {vectorwave-0.2.0 → 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"
|
|
@@ -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
|
|
@@ -461,3 +462,34 @@ def simulate_drift_check(
|
|
|
461
462
|
except Exception as e:
|
|
462
463
|
logger.error(f"Simulation failed: {e}")
|
|
463
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 {}
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
from ..database.db_search import search_functions
|
|
4
4
|
from ..models.db_config import get_weaviate_settings
|
|
5
5
|
from ..search.execution_search import find_by_trace_id
|
|
6
|
+
from ..core.llm.factory import get_llm_client
|
|
6
7
|
|
|
7
8
|
try:
|
|
8
9
|
from openai import OpenAI
|
|
@@ -13,17 +14,7 @@ logger = logging.getLogger(__name__)
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def _get_openai_client():
|
|
16
|
-
|
|
17
|
-
if OpenAI is None:
|
|
18
|
-
logger.error("OpenAI library not found. Install with 'pip install openai'.")
|
|
19
|
-
return None
|
|
20
|
-
|
|
21
|
-
settings = get_weaviate_settings()
|
|
22
|
-
if not settings.OPENAI_API_KEY:
|
|
23
|
-
logger.error("OPENAI_API_KEY is missing in environment variables.")
|
|
24
|
-
return None
|
|
25
|
-
|
|
26
|
-
return OpenAI(api_key=settings.OPENAI_API_KEY)
|
|
17
|
+
return get_llm_client()
|
|
27
18
|
|
|
28
19
|
|
|
29
20
|
def search_and_answer(query: str, model: str = "gpt-4-turbo", language: str = "en") -> str:
|
|
@@ -82,15 +73,16 @@ def search_and_answer(query: str, model: str = "gpt-4-turbo", language: str = "e
|
|
|
82
73
|
return msg if language == 'en' else "❌ OpenAI 클라이언트를 초기화할 수 없습니다. (.env 설정 확인 필요)"
|
|
83
74
|
|
|
84
75
|
try:
|
|
85
|
-
|
|
76
|
+
response_text = client.create_chat_completion(
|
|
86
77
|
model=model,
|
|
87
78
|
messages=[
|
|
88
79
|
{"role": "system", "content": system_instruction},
|
|
89
80
|
{"role": "user", "content": user_message}
|
|
90
81
|
],
|
|
91
|
-
temperature=0.1
|
|
82
|
+
temperature=0.1,
|
|
83
|
+
category="rag_answer"
|
|
92
84
|
)
|
|
93
|
-
return response.
|
|
85
|
+
return response_text if response_text else "❌ Failed to generate response."
|
|
94
86
|
|
|
95
87
|
except Exception as e:
|
|
96
88
|
logger.error(f"LLM generation failed: {e}")
|
|
@@ -146,15 +138,16 @@ def analyze_trace_log(trace_id: str, model: str = "gpt-4-turbo", language: str =
|
|
|
146
138
|
return msg if language == 'en' else "❌ OpenAI 클라이언트 초기화 실패"
|
|
147
139
|
|
|
148
140
|
try:
|
|
149
|
-
|
|
141
|
+
response_text = client.create_chat_completion(
|
|
150
142
|
model=model,
|
|
151
143
|
messages=[
|
|
152
144
|
{"role": "system", "content": system_instruction},
|
|
153
145
|
{"role": "user", "content": log_summary}
|
|
154
146
|
],
|
|
155
|
-
temperature=0.1
|
|
147
|
+
temperature=0.1,
|
|
148
|
+
category="trace_analysis"
|
|
156
149
|
)
|
|
157
|
-
return
|
|
150
|
+
return response_text if response_text else "❌ Failed to generate analysis."
|
|
158
151
|
|
|
159
152
|
except Exception as e:
|
|
160
153
|
error_msg = f"❌ Error during analysis: {e}"
|
|
File without changes
|
|
@@ -7,6 +7,7 @@ from datetime import datetime, timedelta, timezone
|
|
|
7
7
|
from ..search.execution_search import find_executions
|
|
8
8
|
from ..database.db_search import search_functions_hybrid
|
|
9
9
|
from ..models.db_config import get_weaviate_settings
|
|
10
|
+
from ..core.llm.factory import get_llm_client
|
|
10
11
|
|
|
11
12
|
try:
|
|
12
13
|
from openai import OpenAI
|
|
@@ -23,16 +24,7 @@ class VectorWaveHealer:
|
|
|
23
24
|
def __init__(self, model: str = "gpt-4-turbo"):
|
|
24
25
|
self.settings = get_weaviate_settings()
|
|
25
26
|
self.model = model
|
|
26
|
-
self.client =
|
|
27
|
-
|
|
28
|
-
def _init_openai(self) -> Optional[OpenAI]:
|
|
29
|
-
if OpenAI is None:
|
|
30
|
-
logger.error("'openai' library is not installed.")
|
|
31
|
-
return None
|
|
32
|
-
if not self.settings.OPENAI_API_KEY:
|
|
33
|
-
logger.error("OPENAI_API_KEY is not set.")
|
|
34
|
-
return None
|
|
35
|
-
return OpenAI(api_key=self.settings.OPENAI_API_KEY)
|
|
27
|
+
self.client = get_llm_client()
|
|
36
28
|
|
|
37
29
|
def diagnose_and_heal(self, function_name: str, lookback_minutes: int = 60) -> str:
|
|
38
30
|
"""
|
|
@@ -85,15 +77,23 @@ class VectorWaveHealer:
|
|
|
85
77
|
# 5. Call LLM
|
|
86
78
|
print("🤖 Generating fix via LLM...")
|
|
87
79
|
try:
|
|
88
|
-
|
|
80
|
+
response_text = self.client.create_chat_completion(
|
|
89
81
|
model=self.model,
|
|
90
82
|
messages=[
|
|
91
|
-
{"role": "system", "content": "You are an expert Python debugger.
|
|
83
|
+
{"role": "system", "content": "You are an expert Python debugger."
|
|
84
|
+
" Analyze the code and errors provided,"
|
|
85
|
+
" then generate a fixed version of the code."},
|
|
92
86
|
{"role": "user", "content": prompt_context}
|
|
93
87
|
],
|
|
94
|
-
temperature=0.1
|
|
88
|
+
temperature=0.1,
|
|
89
|
+
category="healer"
|
|
95
90
|
)
|
|
96
|
-
|
|
91
|
+
|
|
92
|
+
if response_text:
|
|
93
|
+
return response_text
|
|
94
|
+
else:
|
|
95
|
+
return "❌ LLM returned no response."
|
|
96
|
+
|
|
97
97
|
except Exception as e:
|
|
98
98
|
logger.error(f"LLM generation failed: {e}")
|
|
99
99
|
return f"❌ Error occurred during LLM call: {e}"
|
|
@@ -6,6 +6,7 @@ import math
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import inspect
|
|
8
8
|
from typing import Any, Dict, List, Optional
|
|
9
|
+
from ..core.llm.factory import get_llm_client
|
|
9
10
|
|
|
10
11
|
import weaviate.classes.query as wvc_query
|
|
11
12
|
|
|
@@ -29,13 +30,7 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
29
30
|
|
|
30
31
|
def __init__(self):
|
|
31
32
|
super().__init__()
|
|
32
|
-
self.openai_client =
|
|
33
|
-
|
|
34
|
-
def _init_openai(self) -> Optional[Any]:
|
|
35
|
-
"""Initialize OpenAI client if available and configured."""
|
|
36
|
-
if OpenAI and self.settings.OPENAI_API_KEY:
|
|
37
|
-
return OpenAI(api_key=self.settings.OPENAI_API_KEY)
|
|
38
|
-
return None
|
|
33
|
+
self.openai_client = get_llm_client()
|
|
39
34
|
|
|
40
35
|
def replay(self,
|
|
41
36
|
function_full_name: str,
|
|
@@ -136,7 +131,7 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
136
131
|
"expected": expected_output,
|
|
137
132
|
"actual": actual_output,
|
|
138
133
|
"diff_html": diff_html, # UI visualization field
|
|
139
|
-
"reason": match_reason
|
|
134
|
+
"reason": match_reason # Semantic failure reason
|
|
140
135
|
})
|
|
141
136
|
logger.warning(f"UUID {obj.uuid}: FAILED ({match_reason})")
|
|
142
137
|
|
|
@@ -156,7 +151,8 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
156
151
|
"traceback": traceback.format_exc()
|
|
157
152
|
})
|
|
158
153
|
|
|
159
|
-
logger.info(
|
|
154
|
+
logger.info(
|
|
155
|
+
f"Replay Finished. Passed: {results['passed']}, Failed: {results['failed']}, Updated: {results['updated']}")
|
|
160
156
|
return results
|
|
161
157
|
|
|
162
158
|
def _compare_results_semantic(self, expected: Any, actual: Any,
|
|
@@ -203,6 +199,7 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
203
199
|
norm1 = math.sqrt(sum(a * a for a in v1))
|
|
204
200
|
norm2 = math.sqrt(sum(b * b for b in v2))
|
|
205
201
|
return dot / (norm1 * norm2) if norm1 and norm2 else 0.0
|
|
202
|
+
|
|
206
203
|
except Exception:
|
|
207
204
|
return 0.0
|
|
208
205
|
|
|
@@ -220,13 +217,19 @@ class SemanticReplayer(VectorWaveReplayer):
|
|
|
220
217
|
Respond JSON: {{"equivalent": true}} or {{"equivalent": false}}
|
|
221
218
|
"""
|
|
222
219
|
try:
|
|
223
|
-
|
|
220
|
+
# Refactored to use create_chat_completion
|
|
221
|
+
response_text = self.openai_client.create_chat_completion(
|
|
224
222
|
model="gpt-4-turbo",
|
|
225
223
|
messages=[{"role": "user", "content": prompt}],
|
|
226
224
|
response_format={"type": "json_object"},
|
|
227
|
-
temperature=0.0
|
|
225
|
+
temperature=0.0,
|
|
226
|
+
category="semantic_replay"
|
|
228
227
|
)
|
|
229
|
-
|
|
228
|
+
|
|
229
|
+
if response_text:
|
|
230
|
+
return json.loads(response_text).get("equivalent", False)
|
|
231
|
+
return False
|
|
232
|
+
|
|
230
233
|
except Exception as e:
|
|
231
234
|
logger.error(f"LLM Eval failed: {e}")
|
|
232
|
-
return False
|
|
235
|
+
return False
|
|
File without changes
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from .base import BaseVectorizer
|
|
2
2
|
from typing import List
|
|
3
|
+
from ..core.llm.factory import get_llm_client
|
|
3
4
|
import logging
|
|
4
5
|
|
|
5
6
|
logger = logging.getLogger(__name__)
|
|
@@ -21,16 +22,25 @@ class OpenAIVectorizer(BaseVectorizer):
|
|
|
21
22
|
if not api_key:
|
|
22
23
|
raise ValueError("OpenAI API key is required for OpenAIVectorizer.")
|
|
23
24
|
|
|
24
|
-
self.client =
|
|
25
|
+
self.client = get_llm_client()
|
|
25
26
|
self.model = model
|
|
26
27
|
logger.info("OpenAIVectorizer initialized with model '%s'.", self.model)
|
|
27
28
|
|
|
28
29
|
def embed(self, text: str) -> List[float]:
|
|
29
30
|
text = text.replace("\n", " ")
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
vector = self.client.create_embedding(
|
|
32
|
+
text=text,
|
|
33
|
+
model=self.model,
|
|
34
|
+
category="embedding"
|
|
35
|
+
)
|
|
36
|
+
return vector if vector else []
|
|
32
37
|
|
|
33
38
|
def embed_batch(self, texts: List[str]) -> List[List[float]]:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
embeddings = []
|
|
40
|
+
for text in texts:
|
|
41
|
+
emb = self.embed(text)
|
|
42
|
+
if emb:
|
|
43
|
+
embeddings.append(emb)
|
|
44
|
+
else:
|
|
45
|
+
embeddings.append([])
|
|
46
|
+
return embeddings
|
|
@@ -10,6 +10,8 @@ src/tests/batch/test_batch.py
|
|
|
10
10
|
src/tests/core/__init__.py
|
|
11
11
|
src/tests/core/test_decorator.py
|
|
12
12
|
src/tests/core/test_semantic_caching.py
|
|
13
|
+
src/tests/core/llm/__init__.py
|
|
14
|
+
src/tests/core/llm/test_token_usage.py
|
|
13
15
|
src/tests/database/__init__.py
|
|
14
16
|
src/tests/database/test_archiver.py
|
|
15
17
|
src/tests/database/test_db.py
|
|
@@ -43,6 +45,10 @@ src/vectorwave/core/__init__.py
|
|
|
43
45
|
src/vectorwave/core/core.py
|
|
44
46
|
src/vectorwave/core/decorator.py
|
|
45
47
|
src/vectorwave/core/generator.py
|
|
48
|
+
src/vectorwave/core/llm/__init__.py
|
|
49
|
+
src/vectorwave/core/llm/base.py
|
|
50
|
+
src/vectorwave/core/llm/factory.py
|
|
51
|
+
src/vectorwave/core/llm/openai_client.py
|
|
46
52
|
src/vectorwave/database/__init__.py
|
|
47
53
|
src/vectorwave/database/archiver.py
|
|
48
54
|
src/vectorwave/database/db.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
|
{vectorwave-0.2.0/src/tests/monitoring/alert → vectorwave-0.2.1/src/tests/monitoring}/__init__.py
RENAMED
|
File without changes
|
{vectorwave-0.2.0/src/tests/prediction → vectorwave-0.2.1/src/tests/monitoring/alert}/__init__.py
RENAMED
|
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
|
{vectorwave-0.2.0/src/vectorwave/database → vectorwave-0.2.1/src/vectorwave/core}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vectorwave-0.2.0/src/vectorwave/exception → vectorwave-0.2.1/src/vectorwave/core/llm}/__init__.py
RENAMED
|
File without changes
|
{vectorwave-0.2.0/src/vectorwave/models → vectorwave-0.2.1/src/vectorwave/database}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{vectorwave-0.2.0/src/vectorwave/monitoring → vectorwave-0.2.1/src/vectorwave/exception}/__init__.py
RENAMED
|
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
|
{vectorwave-0.2.0/src/vectorwave/utils → vectorwave-0.2.1/src/vectorwave/prediction}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{vectorwave-0.2.0/src/vectorwave/vectorizer → vectorwave-0.2.1/src/vectorwave/search}/__init__.py
RENAMED
|
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
|