vector-inspector 0.2.6__py3-none-any.whl → 0.2.7__py3-none-any.whl
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.
- vector_inspector/core/cache_manager.py +159 -0
- vector_inspector/core/connection_manager.py +277 -0
- vector_inspector/core/connections/chroma_connection.py +90 -5
- vector_inspector/core/connections/qdrant_connection.py +62 -8
- vector_inspector/core/embedding_utils.py +140 -0
- vector_inspector/services/backup_restore_service.py +3 -29
- vector_inspector/services/credential_service.py +130 -0
- vector_inspector/services/filter_service.py +1 -1
- vector_inspector/services/profile_service.py +409 -0
- vector_inspector/services/settings_service.py +19 -0
- vector_inspector/ui/components/connection_manager_panel.py +320 -0
- vector_inspector/ui/components/profile_manager_panel.py +518 -0
- vector_inspector/ui/dialogs/__init__.py +5 -0
- vector_inspector/ui/dialogs/cross_db_migration.py +364 -0
- vector_inspector/ui/dialogs/embedding_config_dialog.py +176 -0
- vector_inspector/ui/main_window.py +425 -190
- vector_inspector/ui/views/info_panel.py +225 -55
- vector_inspector/ui/views/metadata_view.py +71 -3
- vector_inspector/ui/views/search_view.py +43 -3
- {vector_inspector-0.2.6.dist-info → vector_inspector-0.2.7.dist-info}/METADATA +3 -1
- vector_inspector-0.2.7.dist-info/RECORD +45 -0
- vector_inspector-0.2.6.dist-info/RECORD +0 -35
- {vector_inspector-0.2.6.dist-info → vector_inspector-0.2.7.dist-info}/WHEEL +0 -0
- {vector_inspector-0.2.6.dist-info → vector_inspector-0.2.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Service for managing connection profiles."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Dict, Any, Optional
|
|
7
|
+
from PySide6.QtCore import QObject, Signal
|
|
8
|
+
|
|
9
|
+
from .credential_service import CredentialService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConnectionProfile:
|
|
13
|
+
"""Represents a saved connection profile."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
profile_id: str,
|
|
18
|
+
name: str,
|
|
19
|
+
provider: str,
|
|
20
|
+
config: Dict[str, Any],
|
|
21
|
+
credential_fields: Optional[List[str]] = None
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize a connection profile.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
profile_id: Unique profile identifier
|
|
28
|
+
name: User-friendly profile name
|
|
29
|
+
provider: Provider type (chromadb, qdrant, etc.)
|
|
30
|
+
config: Non-sensitive configuration (host, port, path, etc.)
|
|
31
|
+
credential_fields: List of field names that contain credentials
|
|
32
|
+
"""
|
|
33
|
+
self.id = profile_id
|
|
34
|
+
self.name = name
|
|
35
|
+
self.provider = provider
|
|
36
|
+
self.config = config
|
|
37
|
+
self.credential_fields = credential_fields or []
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
40
|
+
"""Convert profile to dictionary (without credentials)."""
|
|
41
|
+
return {
|
|
42
|
+
"id": self.id,
|
|
43
|
+
"name": self.name,
|
|
44
|
+
"provider": self.provider,
|
|
45
|
+
"config": self.config,
|
|
46
|
+
"credential_fields": self.credential_fields
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ConnectionProfile":
|
|
51
|
+
"""Create profile from dictionary."""
|
|
52
|
+
return cls(
|
|
53
|
+
profile_id=data["id"],
|
|
54
|
+
name=data["name"],
|
|
55
|
+
provider=data["provider"],
|
|
56
|
+
config=data.get("config", {}),
|
|
57
|
+
credential_fields=data.get("credential_fields", [])
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProfileService(QObject):
|
|
62
|
+
"""Manages connection profiles and persistence.
|
|
63
|
+
|
|
64
|
+
Signals:
|
|
65
|
+
profile_added: Emitted when a profile is added (profile_id)
|
|
66
|
+
profile_updated: Emitted when a profile is updated (profile_id)
|
|
67
|
+
profile_deleted: Emitted when a profile is deleted (profile_id)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Signals
|
|
71
|
+
profile_added = Signal(str) # profile_id
|
|
72
|
+
profile_updated = Signal(str) # profile_id
|
|
73
|
+
profile_deleted = Signal(str) # profile_id
|
|
74
|
+
|
|
75
|
+
def __init__(self):
|
|
76
|
+
"""Initialize profile service."""
|
|
77
|
+
super().__init__()
|
|
78
|
+
self.profiles_dir = Path.home() / ".vector-inspector"
|
|
79
|
+
self.profiles_file = self.profiles_dir / "profiles.json"
|
|
80
|
+
self.credential_service = CredentialService()
|
|
81
|
+
self._profiles: Dict[str, ConnectionProfile] = {}
|
|
82
|
+
self._last_active_connections: List[str] = []
|
|
83
|
+
self._load_profiles()
|
|
84
|
+
|
|
85
|
+
def _load_profiles(self):
|
|
86
|
+
"""Load profiles from disk."""
|
|
87
|
+
try:
|
|
88
|
+
if self.profiles_file.exists():
|
|
89
|
+
with open(self.profiles_file, 'r', encoding='utf-8') as f:
|
|
90
|
+
data = json.load(f)
|
|
91
|
+
|
|
92
|
+
# Load profiles
|
|
93
|
+
for profile_data in data.get("profiles", []):
|
|
94
|
+
profile = ConnectionProfile.from_dict(profile_data)
|
|
95
|
+
self._profiles[profile.id] = profile
|
|
96
|
+
|
|
97
|
+
# Load last active connections
|
|
98
|
+
self._last_active_connections = data.get("last_active_connections", [])
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"Failed to load profiles: {e}")
|
|
101
|
+
self._profiles = {}
|
|
102
|
+
self._last_active_connections = []
|
|
103
|
+
|
|
104
|
+
def _save_profiles(self):
|
|
105
|
+
"""Save profiles to disk."""
|
|
106
|
+
try:
|
|
107
|
+
# Create directory if it doesn't exist
|
|
108
|
+
self.profiles_dir.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
# Prepare data
|
|
111
|
+
data = {
|
|
112
|
+
"profiles": [profile.to_dict() for profile in self._profiles.values()],
|
|
113
|
+
"last_active_connections": self._last_active_connections
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Write to file
|
|
117
|
+
with open(self.profiles_file, 'w', encoding='utf-8') as f:
|
|
118
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"Failed to save profiles: {e}")
|
|
121
|
+
|
|
122
|
+
def create_profile(
|
|
123
|
+
self,
|
|
124
|
+
name: str,
|
|
125
|
+
provider: str,
|
|
126
|
+
config: Dict[str, Any],
|
|
127
|
+
credentials: Optional[Dict[str, Any]] = None
|
|
128
|
+
) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Create a new connection profile.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
name: Profile name
|
|
134
|
+
provider: Provider type
|
|
135
|
+
config: Connection configuration (non-sensitive)
|
|
136
|
+
credentials: Credential data to store securely (optional)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The profile ID
|
|
140
|
+
"""
|
|
141
|
+
profile_id = str(uuid.uuid4())
|
|
142
|
+
credential_fields = list(credentials.keys()) if credentials else []
|
|
143
|
+
|
|
144
|
+
profile = ConnectionProfile(
|
|
145
|
+
profile_id=profile_id,
|
|
146
|
+
name=name,
|
|
147
|
+
provider=provider,
|
|
148
|
+
config=config,
|
|
149
|
+
credential_fields=credential_fields
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
self._profiles[profile_id] = profile
|
|
153
|
+
|
|
154
|
+
# Store credentials if provided
|
|
155
|
+
if credentials:
|
|
156
|
+
self.credential_service.store_credentials(profile_id, credentials)
|
|
157
|
+
|
|
158
|
+
self._save_profiles()
|
|
159
|
+
self.profile_added.emit(profile_id)
|
|
160
|
+
|
|
161
|
+
return profile_id
|
|
162
|
+
|
|
163
|
+
def get_profile(self, profile_id: str) -> Optional[ConnectionProfile]:
|
|
164
|
+
"""Get a profile by ID."""
|
|
165
|
+
return self._profiles.get(profile_id)
|
|
166
|
+
|
|
167
|
+
def get_all_profiles(self) -> List[ConnectionProfile]:
|
|
168
|
+
"""Get all saved profiles."""
|
|
169
|
+
return list(self._profiles.values())
|
|
170
|
+
|
|
171
|
+
def update_profile(
|
|
172
|
+
self,
|
|
173
|
+
profile_id: str,
|
|
174
|
+
name: Optional[str] = None,
|
|
175
|
+
config: Optional[Dict[str, Any]] = None,
|
|
176
|
+
credentials: Optional[Dict[str, Any]] = None
|
|
177
|
+
) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Update an existing profile.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
profile_id: ID of profile to update
|
|
183
|
+
name: New name (optional)
|
|
184
|
+
config: New configuration (optional)
|
|
185
|
+
credentials: New credentials (optional)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if successful, False if profile not found
|
|
189
|
+
"""
|
|
190
|
+
profile = self._profiles.get(profile_id)
|
|
191
|
+
if not profile:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
if name is not None:
|
|
195
|
+
profile.name = name
|
|
196
|
+
|
|
197
|
+
if config is not None:
|
|
198
|
+
profile.config = config
|
|
199
|
+
|
|
200
|
+
if credentials is not None:
|
|
201
|
+
profile.credential_fields = list(credentials.keys())
|
|
202
|
+
self.credential_service.store_credentials(profile_id, credentials)
|
|
203
|
+
|
|
204
|
+
self._save_profiles()
|
|
205
|
+
self.profile_updated.emit(profile_id)
|
|
206
|
+
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
def delete_profile(self, profile_id: str) -> bool:
|
|
210
|
+
"""
|
|
211
|
+
Delete a profile.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
profile_id: ID of profile to delete
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
True if successful, False if profile not found
|
|
218
|
+
"""
|
|
219
|
+
if profile_id not in self._profiles:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
# Delete credentials
|
|
223
|
+
self.credential_service.delete_credentials(profile_id)
|
|
224
|
+
|
|
225
|
+
# Remove from profiles
|
|
226
|
+
del self._profiles[profile_id]
|
|
227
|
+
|
|
228
|
+
# Remove from last active connections if present
|
|
229
|
+
if profile_id in self._last_active_connections:
|
|
230
|
+
self._last_active_connections.remove(profile_id)
|
|
231
|
+
|
|
232
|
+
self._save_profiles()
|
|
233
|
+
self.profile_deleted.emit(profile_id)
|
|
234
|
+
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
def duplicate_profile(self, profile_id: str, new_name: str) -> Optional[str]:
|
|
238
|
+
"""
|
|
239
|
+
Duplicate an existing profile.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
profile_id: ID of profile to duplicate
|
|
243
|
+
new_name: Name for the new profile
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
New profile ID, or None if source profile not found
|
|
247
|
+
"""
|
|
248
|
+
source_profile = self._profiles.get(profile_id)
|
|
249
|
+
if not source_profile:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
# Get credentials from source
|
|
253
|
+
credentials = self.credential_service.get_credentials(profile_id)
|
|
254
|
+
|
|
255
|
+
# Create new profile
|
|
256
|
+
new_id = self.create_profile(
|
|
257
|
+
name=new_name,
|
|
258
|
+
provider=source_profile.provider,
|
|
259
|
+
config=source_profile.config.copy(),
|
|
260
|
+
credentials=credentials
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return new_id
|
|
264
|
+
|
|
265
|
+
def get_profile_with_credentials(self, profile_id: str) -> Optional[Dict[str, Any]]:
|
|
266
|
+
"""
|
|
267
|
+
Get a profile along with its credentials.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
profile_id: ID of profile
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Dictionary with profile data and credentials, or None if not found
|
|
274
|
+
"""
|
|
275
|
+
profile = self._profiles.get(profile_id)
|
|
276
|
+
if not profile:
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
credentials = self.credential_service.get_credentials(profile_id)
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
"id": profile.id,
|
|
283
|
+
"name": profile.name,
|
|
284
|
+
"provider": profile.provider,
|
|
285
|
+
"config": profile.config,
|
|
286
|
+
"credentials": credentials or {}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
def export_profiles(self, include_credentials: bool = False) -> List[Dict[str, Any]]:
|
|
290
|
+
"""
|
|
291
|
+
Export all profiles for backup/sharing.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
include_credentials: Whether to include credentials (NOT RECOMMENDED)
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
List of profile dictionaries
|
|
298
|
+
"""
|
|
299
|
+
exported = []
|
|
300
|
+
for profile in self._profiles.values():
|
|
301
|
+
data = profile.to_dict()
|
|
302
|
+
if include_credentials:
|
|
303
|
+
credentials = self.credential_service.get_credentials(profile.id)
|
|
304
|
+
if credentials:
|
|
305
|
+
data["credentials"] = credentials
|
|
306
|
+
exported.append(data)
|
|
307
|
+
return exported
|
|
308
|
+
|
|
309
|
+
def import_profiles(
|
|
310
|
+
self,
|
|
311
|
+
profiles_data: List[Dict[str, Any]],
|
|
312
|
+
overwrite: bool = False
|
|
313
|
+
) -> Dict[str, str]:
|
|
314
|
+
"""
|
|
315
|
+
Import profiles from exported data.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
profiles_data: List of profile dictionaries
|
|
319
|
+
overwrite: Whether to overwrite existing profiles with same ID
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Dictionary mapping old IDs to new IDs
|
|
323
|
+
"""
|
|
324
|
+
id_mapping = {}
|
|
325
|
+
|
|
326
|
+
for profile_data in profiles_data:
|
|
327
|
+
old_id = profile_data.get("id")
|
|
328
|
+
|
|
329
|
+
# Generate new ID if not overwriting or ID exists
|
|
330
|
+
if not overwrite or old_id in self._profiles:
|
|
331
|
+
new_id = str(uuid.uuid4())
|
|
332
|
+
else:
|
|
333
|
+
new_id = str(old_id) if old_id else str(uuid.uuid4())
|
|
334
|
+
|
|
335
|
+
credentials = profile_data.pop("credentials", None)
|
|
336
|
+
|
|
337
|
+
# Create profile
|
|
338
|
+
profile = ConnectionProfile(
|
|
339
|
+
profile_id=new_id,
|
|
340
|
+
name=profile_data["name"],
|
|
341
|
+
provider=profile_data["provider"],
|
|
342
|
+
config=profile_data.get("config", {}),
|
|
343
|
+
credential_fields=profile_data.get("credential_fields", [])
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
self._profiles[new_id] = profile
|
|
347
|
+
|
|
348
|
+
# Store credentials if provided
|
|
349
|
+
if credentials:
|
|
350
|
+
self.credential_service.store_credentials(new_id, credentials)
|
|
351
|
+
|
|
352
|
+
id_mapping[old_id] = new_id
|
|
353
|
+
|
|
354
|
+
self._save_profiles()
|
|
355
|
+
|
|
356
|
+
return id_mapping
|
|
357
|
+
|
|
358
|
+
def save_last_active_connections(self, connection_ids: List[str]):
|
|
359
|
+
"""
|
|
360
|
+
Save list of last active connection profile IDs for session restore.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
connection_ids: List of profile IDs that were active
|
|
364
|
+
"""
|
|
365
|
+
self._last_active_connections = connection_ids
|
|
366
|
+
self._save_profiles()
|
|
367
|
+
|
|
368
|
+
def get_last_active_connections(self) -> List[str]:
|
|
369
|
+
"""Get list of last active connection profile IDs."""
|
|
370
|
+
return self._last_active_connections.copy()
|
|
371
|
+
|
|
372
|
+
def migrate_legacy_connection(self, config: Dict[str, Any]) -> str:
|
|
373
|
+
"""
|
|
374
|
+
Migrate a legacy single-connection configuration to a profile.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
config: Legacy connection configuration
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
The new profile ID
|
|
381
|
+
"""
|
|
382
|
+
provider = config.get("provider", "chromadb")
|
|
383
|
+
conn_type = config.get("type", "persistent")
|
|
384
|
+
|
|
385
|
+
# Create a name based on connection type
|
|
386
|
+
if conn_type == "persistent":
|
|
387
|
+
name = f"Legacy {provider.title()} (Persistent)"
|
|
388
|
+
elif conn_type == "http":
|
|
389
|
+
host = config.get("host", "localhost")
|
|
390
|
+
name = f"Legacy {provider.title()} ({host})"
|
|
391
|
+
else:
|
|
392
|
+
name = f"Legacy {provider.title()} (Ephemeral)"
|
|
393
|
+
|
|
394
|
+
# Extract credentials if any
|
|
395
|
+
credentials = {}
|
|
396
|
+
if "api_key" in config and config["api_key"]:
|
|
397
|
+
credentials["api_key"] = config["api_key"]
|
|
398
|
+
del config["api_key"] # Remove from config
|
|
399
|
+
|
|
400
|
+
# Create profile
|
|
401
|
+
profile_id = self.create_profile(
|
|
402
|
+
name=name,
|
|
403
|
+
provider=provider,
|
|
404
|
+
config=config,
|
|
405
|
+
credentials=credentials if credentials else None
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
return profile_id
|
|
409
|
+
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Dict, Any, Optional
|
|
6
|
+
from vector_inspector.core.cache_manager import invalidate_cache_on_settings_change
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class SettingsService:
|
|
@@ -49,10 +50,28 @@ class SettingsService:
|
|
|
49
50
|
"""Get a setting value."""
|
|
50
51
|
return self.settings.get(key, default)
|
|
51
52
|
|
|
53
|
+
def get_cache_enabled(self) -> bool:
|
|
54
|
+
"""Get whether caching is enabled (default: True)."""
|
|
55
|
+
return self.settings.get("cache_enabled", True)
|
|
56
|
+
|
|
57
|
+
def set_cache_enabled(self, enabled: bool):
|
|
58
|
+
"""Set whether caching is enabled."""
|
|
59
|
+
self.set("cache_enabled", enabled)
|
|
60
|
+
# Update cache manager state
|
|
61
|
+
from vector_inspector.core.cache_manager import get_cache_manager
|
|
62
|
+
cache = get_cache_manager()
|
|
63
|
+
if enabled:
|
|
64
|
+
cache.enable()
|
|
65
|
+
else:
|
|
66
|
+
cache.disable()
|
|
67
|
+
|
|
52
68
|
def set(self, key: str, value: Any):
|
|
53
69
|
"""Set a setting value."""
|
|
54
70
|
self.settings[key] = value
|
|
55
71
|
self._save_settings()
|
|
72
|
+
# Invalidate cache when settings change (only if cache is enabled)
|
|
73
|
+
if key != "cache_enabled": # Don't invalidate when toggling cache itself
|
|
74
|
+
invalidate_cache_on_settings_change()
|
|
56
75
|
|
|
57
76
|
def clear(self):
|
|
58
77
|
"""Clear all settings."""
|