openchadpy 0.1.0__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.
- openchadpy/Tauri.toml +20 -0
- openchadpy/__init__.py +1 -0
- openchadpy/backend_registry.py +356 -0
- openchadpy/base_backend.py +41 -0
- openchadpy/base_embedding.py +116 -0
- openchadpy/base_lm.py +60 -0
- openchadpy/base_provider.py +74 -0
- openchadpy/base_stt.py +95 -0
- openchadpy/base_tts.py +46 -0
- openchadpy/base_vision.py +91 -0
- openchadpy/capabilities/default.json +52 -0
- openchadpy/connection_manager.py +64 -0
- openchadpy/context.py +8 -0
- openchadpy/credentials.py +150 -0
- openchadpy/database.py +258 -0
- openchadpy/database_manager.py +93 -0
- openchadpy/event_emitter.py +313 -0
- openchadpy/file.py +233 -0
- openchadpy/file_manager.py +236 -0
- openchadpy/icons/icon.ico +0 -0
- openchadpy/icons/icon.png +0 -0
- openchadpy/main.py +1987 -0
- openchadpy/mcp_manager.py +417 -0
- openchadpy/model_manager.py +1154 -0
- openchadpy/model_provider.py +291 -0
- openchadpy/pipeline_base.py +206 -0
- openchadpy/pipeline_manager.py +320 -0
- openchadpy/plugin_watcher.py +385 -0
- openchadpy/process_audio.py +95 -0
- openchadpy/process_image.py +86 -0
- openchadpy/process_video.py +129 -0
- openchadpy/proxy.py +82 -0
- openchadpy/settings.py +346 -0
- openchadpy/settings_subscription.py +48 -0
- openchadpy/sqlite.py +185 -0
- openchadpy/startup.py +68 -0
- openchadpy/streaming_response.py +151 -0
- openchadpy/tool_base.py +253 -0
- openchadpy/tool_manager.py +442 -0
- openchadpy/vram_checker.py +95 -0
- openchadpy/webrtc_client.py +188 -0
- openchadpy/websocket_client.py +29 -0
- openchadpy-0.1.0.dist-info/METADATA +37 -0
- openchadpy-0.1.0.dist-info/RECORD +47 -0
- openchadpy-0.1.0.dist-info/WHEEL +4 -0
- openchadpy-0.1.0.dist-info/entry_points.txt +2 -0
- openchadpy-0.1.0.dist-info/licenses/LICENSE +201 -0
openchadpy/Tauri.toml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"$schema" = "https://schema.tauri.app/config/2"
|
|
2
|
+
productName = "openchad"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
identifier = "com.openchad.app"
|
|
5
|
+
[build]
|
|
6
|
+
frontendDist = "./frontend"
|
|
7
|
+
[app]
|
|
8
|
+
withGlobalTauri = true
|
|
9
|
+
security.capabilities = ["default"]
|
|
10
|
+
[[app.windows]]
|
|
11
|
+
title = "openchad"
|
|
12
|
+
width = 1680
|
|
13
|
+
height = 945
|
|
14
|
+
decorations = false
|
|
15
|
+
maximized = true
|
|
16
|
+
[bundle]
|
|
17
|
+
icon = [
|
|
18
|
+
"icons/icon.png",
|
|
19
|
+
"icons/icon.ico",
|
|
20
|
+
]
|
openchadpy/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Mark as a package for regular module import resolution
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend Registry Module
|
|
3
|
+
Provides plugin discovery and registration for backend implementations.
|
|
4
|
+
Automatically discovers backends from the Backend/ directory.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import importlib
|
|
11
|
+
import importlib.util
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Optional, Type, Any, Set
|
|
14
|
+
from .base_backend import BaseBackend, BackendMetadata
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
class BackendRegistry:
|
|
17
|
+
"""
|
|
18
|
+
Discovers and manages backend plugins from the Backend/ directory.
|
|
19
|
+
Each backend must have:
|
|
20
|
+
- main.py with a class inheriting from BaseBackend (or its subclasses)
|
|
21
|
+
- Optionally: manifest.json with metadata
|
|
22
|
+
Example usage:
|
|
23
|
+
registry = BackendRegistry("../Backend")
|
|
24
|
+
await registry.discover()
|
|
25
|
+
# List all backends
|
|
26
|
+
for meta in registry.list_backends():
|
|
27
|
+
print(meta.name, meta.capabilities)
|
|
28
|
+
# Create instance
|
|
29
|
+
model = registry.create_instance("SentenceTransformers", model_path="...")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, backends_dir: str):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the registry.
|
|
35
|
+
Args:
|
|
36
|
+
backends_dir: Path to the Backend/ directory
|
|
37
|
+
"""
|
|
38
|
+
if os.path.isabs(backends_dir):
|
|
39
|
+
self._backends_dir = Path(backends_dir).resolve()
|
|
40
|
+
else:
|
|
41
|
+
self._backends_dir = Path(backends_dir).resolve()
|
|
42
|
+
self._backends: Dict[str, Type[BaseBackend]] = {}
|
|
43
|
+
self._metadata: Dict[str, BackendMetadata] = {}
|
|
44
|
+
self._backend_paths: Dict[str, Path] = {} # New: store paths for lazy loading
|
|
45
|
+
self._discovered = False
|
|
46
|
+
|
|
47
|
+
async def discover(self) -> Dict[str, BackendMetadata]:
|
|
48
|
+
"""
|
|
49
|
+
Scan backends directory and identify available backends without fully loading them.
|
|
50
|
+
Uses manifest.json if available for metadata.
|
|
51
|
+
Directory structure: Backend/{publisher}/{plugin}/main.py
|
|
52
|
+
Keys use format: {publisher}/{plugin} to prevent naming conflicts.
|
|
53
|
+
"""
|
|
54
|
+
if not self._backends_dir.exists():
|
|
55
|
+
logger.warning(f"Backends directory not found: {self._backends_dir}")
|
|
56
|
+
return {}
|
|
57
|
+
logger.info(f"Discovering backends in: {self._backends_dir}")
|
|
58
|
+
# Iterate through publisher directories
|
|
59
|
+
for publisher_dir in self._backends_dir.iterdir():
|
|
60
|
+
try:
|
|
61
|
+
if not publisher_dir.is_dir():
|
|
62
|
+
continue
|
|
63
|
+
# Skip directories starting with underscore or dot
|
|
64
|
+
if publisher_dir.name.startswith(('_', '.')):
|
|
65
|
+
continue
|
|
66
|
+
publisher_name = publisher_dir.name
|
|
67
|
+
# Iterate through plugin directories within publisher
|
|
68
|
+
for plugin_dir in publisher_dir.iterdir():
|
|
69
|
+
try:
|
|
70
|
+
if not plugin_dir.is_dir():
|
|
71
|
+
continue
|
|
72
|
+
# Skip directories starting with underscore or dot
|
|
73
|
+
if plugin_dir.name.startswith(('_', '.')):
|
|
74
|
+
continue
|
|
75
|
+
main_py = plugin_dir / "main.py"
|
|
76
|
+
manifest_path = plugin_dir / "manifest.json"
|
|
77
|
+
if not main_py.exists():
|
|
78
|
+
logger.debug(f"Skipping {publisher_name}/{plugin_dir.name}: no main.py found")
|
|
79
|
+
continue
|
|
80
|
+
plugin_name = plugin_dir.name
|
|
81
|
+
# 1. Try to get identity/metadata from manifest first (fast)
|
|
82
|
+
metadata = None
|
|
83
|
+
if manifest_path.exists():
|
|
84
|
+
try:
|
|
85
|
+
metadata = self._load_metadata_from_manifest(manifest_path, plugin_name)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning(f"Failed to read manifest for {publisher_name}/{plugin_name}: {e}")
|
|
88
|
+
# 2. If no manifest, provide minimal metadata until loaded
|
|
89
|
+
if metadata is None:
|
|
90
|
+
metadata = BackendMetadata(
|
|
91
|
+
name=plugin_name,
|
|
92
|
+
version="1.0.0",
|
|
93
|
+
description=f"Lazy-loaded backend: {publisher_name}/{plugin_name}",
|
|
94
|
+
capabilities=[],
|
|
95
|
+
author=publisher_name
|
|
96
|
+
)
|
|
97
|
+
# Discovery key uses publisher/plugin format to prevent conflicts
|
|
98
|
+
bid = f"{publisher_name}/{plugin_name}".lower()
|
|
99
|
+
self._backend_paths[bid] = plugin_dir
|
|
100
|
+
self._metadata[bid] = metadata
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"Error discovering plugin in {publisher_dir.name}: {e}")
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Error scanning publisher directory {publisher_dir.name}: {e}")
|
|
105
|
+
self._discovered = True
|
|
106
|
+
logger.info(f"Discovered {len(self._backend_paths)} potential backends.")
|
|
107
|
+
return self._metadata.copy()
|
|
108
|
+
|
|
109
|
+
def _load_metadata_from_manifest(self, manifest_path: Path, fallback_name: str) -> BackendMetadata:
|
|
110
|
+
"""Helper to read manifest without loading class."""
|
|
111
|
+
with open(manifest_path, 'r') as f:
|
|
112
|
+
data = json.load(f)
|
|
113
|
+
caps = []
|
|
114
|
+
for cap_name in data.get('capabilities', []):
|
|
115
|
+
try:
|
|
116
|
+
caps.append(cap_name)
|
|
117
|
+
except KeyError:
|
|
118
|
+
logger.warning(f"Unknown capability: {cap_name}")
|
|
119
|
+
return BackendMetadata(
|
|
120
|
+
name=data.get('name', fallback_name),
|
|
121
|
+
version=data.get('version', '1.0.0'),
|
|
122
|
+
description=data.get('description', ''),
|
|
123
|
+
capabilities=caps,
|
|
124
|
+
author=data.get('author', ''),
|
|
125
|
+
requirements=data.get('requirements', [])
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def _load_backend_by_name(self, name: str):
|
|
129
|
+
"""Perform actual module load and class extraction.
|
|
130
|
+
Args:
|
|
131
|
+
name: Backend key in format 'publisher/plugin'
|
|
132
|
+
"""
|
|
133
|
+
backend_dir = self._backend_paths.get(name.lower())
|
|
134
|
+
if not backend_dir:
|
|
135
|
+
raise ValueError(f"Backend '{name}' not found in discovery map.")
|
|
136
|
+
# Extract publisher and plugin names from path
|
|
137
|
+
plugin_name = backend_dir.name
|
|
138
|
+
publisher_name = backend_dir.parent.name
|
|
139
|
+
main_py = backend_dir / "main.py"
|
|
140
|
+
manifest_path = backend_dir / "manifest.json"
|
|
141
|
+
# Ensure plugin paths are in sys.path
|
|
142
|
+
plugin_path = str(backend_dir.resolve())
|
|
143
|
+
if plugin_path not in sys.path:
|
|
144
|
+
logger.debug(f"Adding plugin path to sys.path: {plugin_path}")
|
|
145
|
+
sys.path.insert(0, plugin_path)
|
|
146
|
+
# Add project python directory
|
|
147
|
+
python_path = str(Path(os.environ.get("OPENCHAD_UV_PROJECT_DIR")).resolve()) #pyrefly: ignore
|
|
148
|
+
if python_path not in sys.path:
|
|
149
|
+
logger.debug(f"Adding python path to sys.path: {python_path}")
|
|
150
|
+
sys.path.insert(0, python_path)
|
|
151
|
+
logger.info(f"Lazy-loading backend code: {publisher_name}/{plugin_name}")
|
|
152
|
+
# Use publisher_plugin format for module name to prevent conflicts
|
|
153
|
+
module_name = f"backend_{publisher_name}_{plugin_name}"
|
|
154
|
+
try:
|
|
155
|
+
# Load the module
|
|
156
|
+
spec = importlib.util.spec_from_file_location(
|
|
157
|
+
module_name,
|
|
158
|
+
main_py
|
|
159
|
+
)
|
|
160
|
+
if spec is None or spec.loader is None:
|
|
161
|
+
raise ImportError(f"Cannot load module from {main_py}")
|
|
162
|
+
module = importlib.util.module_from_spec(spec)
|
|
163
|
+
sys.modules[module_name] = module
|
|
164
|
+
# Use a thread for exec_module as it might be slow, though here we want it to be part of the flow
|
|
165
|
+
spec.loader.exec_module(module)
|
|
166
|
+
# Find the backend class
|
|
167
|
+
backend_class = None
|
|
168
|
+
for attr_name in dir(module):
|
|
169
|
+
attr = getattr(module, attr_name)
|
|
170
|
+
if (
|
|
171
|
+
isinstance(attr, type)
|
|
172
|
+
and hasattr(attr, 'backend')
|
|
173
|
+
and getattr(attr, 'backend', '') != ''
|
|
174
|
+
and attr.__module__ == module.__name__
|
|
175
|
+
):
|
|
176
|
+
backend_class = attr
|
|
177
|
+
break
|
|
178
|
+
if backend_class is None:
|
|
179
|
+
raise ValueError(f"No valid backend class found in {main_py}")
|
|
180
|
+
# Extract class backend id
|
|
181
|
+
actual_bid = getattr(backend_class, 'backend')
|
|
182
|
+
# Update metadata if it was minimal or needs class verification
|
|
183
|
+
metadata = self._get_metadata(backend_class, manifest_path, plugin_name)
|
|
184
|
+
# Register in final maps using the discovery key (publisher/plugin)
|
|
185
|
+
self._backends[name.lower()] = backend_class
|
|
186
|
+
self._metadata[name.lower()] = metadata
|
|
187
|
+
logger.info(f"Registered backend: {name} (class id: {actual_bid})")
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Failed to load backend module {module_name}: {e}", exc_info=True)
|
|
190
|
+
# Clean up if partially loaded
|
|
191
|
+
if module_name in sys.modules:
|
|
192
|
+
del sys.modules[module_name]
|
|
193
|
+
raise RuntimeError(f"Backend module loading failed: {e}") from e
|
|
194
|
+
|
|
195
|
+
async def get_backend_class(self, name: str) -> Optional[Type[BaseBackend]]:
|
|
196
|
+
"""Get a backend class by name, loading it if necessary."""
|
|
197
|
+
# 1. Try already loaded
|
|
198
|
+
if name in self._backends:
|
|
199
|
+
return self._backends[name]
|
|
200
|
+
# 2. Try loading from discovery map
|
|
201
|
+
search_name = name.lower()
|
|
202
|
+
if search_name in self._backend_paths:
|
|
203
|
+
try:
|
|
204
|
+
await self._load_backend_by_name(search_name)
|
|
205
|
+
# Return by original name or the normalized search name
|
|
206
|
+
return self._backends.get(name) or self._backends.get(search_name)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Failed to lazy-load backend {name}: {e}", exc_info=True)
|
|
209
|
+
return None
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def get_metadata(self, name: str) -> Optional[BackendMetadata]:
|
|
213
|
+
"""Get metadata for a backend."""
|
|
214
|
+
search_name = name.lower()
|
|
215
|
+
return self._metadata.get(search_name) or self._metadata.get(name)
|
|
216
|
+
|
|
217
|
+
def list_backends(self) -> List[BackendMetadata]:
|
|
218
|
+
"""List all registered or discovered backends."""
|
|
219
|
+
return list(self._metadata.values())
|
|
220
|
+
|
|
221
|
+
def get_backends_by_capability(self, capability: str) -> List[str]:
|
|
222
|
+
"""Get all backends that support a given capability."""
|
|
223
|
+
result = []
|
|
224
|
+
for name, meta in self._metadata.items():
|
|
225
|
+
if capability in meta.capabilities:
|
|
226
|
+
result.append(name)
|
|
227
|
+
return list(set(result)) # Unique names
|
|
228
|
+
|
|
229
|
+
async def create_instance(self, name: str, **kwargs) -> BaseBackend:
|
|
230
|
+
"""
|
|
231
|
+
Create an instance of a backend, loading it if necessary.
|
|
232
|
+
"""
|
|
233
|
+
backend_class = await self.get_backend_class(name)
|
|
234
|
+
if backend_class is None:
|
|
235
|
+
raise ValueError(f"Unknown or failed to load backend: {name}")
|
|
236
|
+
return backend_class(**kwargs)
|
|
237
|
+
|
|
238
|
+
def has_backend(self, name: str) -> bool:
|
|
239
|
+
"""Check if a backend is registered or discoverable."""
|
|
240
|
+
search_name = name.lower()
|
|
241
|
+
return search_name in self._backends or search_name in self._backend_paths or name in self._backends
|
|
242
|
+
|
|
243
|
+
def _get_metadata(
|
|
244
|
+
self,
|
|
245
|
+
backend_class: Type,
|
|
246
|
+
manifest_path: Path,
|
|
247
|
+
fallback_name: str
|
|
248
|
+
) -> BackendMetadata:
|
|
249
|
+
"""Extract metadata from class or manifest file."""
|
|
250
|
+
# Try manifest.json first
|
|
251
|
+
if manifest_path.exists():
|
|
252
|
+
try:
|
|
253
|
+
with open(manifest_path, 'r') as f:
|
|
254
|
+
data = json.load(f)
|
|
255
|
+
# Parse capabilities
|
|
256
|
+
caps = []
|
|
257
|
+
for cap_name in data.get('capabilities', []):
|
|
258
|
+
try:
|
|
259
|
+
caps.append(cap_name)
|
|
260
|
+
except KeyError:
|
|
261
|
+
logger.warning(f"Unknown capability: {cap_name}")
|
|
262
|
+
return BackendMetadata(
|
|
263
|
+
name=data.get('name', fallback_name),
|
|
264
|
+
version=data.get('version', '1.0.0'),
|
|
265
|
+
description=data.get('description', ''),
|
|
266
|
+
capabilities=caps,
|
|
267
|
+
author=data.get('author', ''),
|
|
268
|
+
requirements=data.get('requirements', [])
|
|
269
|
+
)
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.warning(f"Failed to read manifest: {e}")
|
|
272
|
+
# Try class metadata property (if implemented)
|
|
273
|
+
if hasattr(backend_class, 'metadata') and not callable(getattr(backend_class, 'metadata', None)):
|
|
274
|
+
# It's a property, we'd need an instance - skip for now
|
|
275
|
+
pass
|
|
276
|
+
# Generate minimal metadata from class
|
|
277
|
+
return BackendMetadata(
|
|
278
|
+
name=getattr(backend_class, 'backend', fallback_name),
|
|
279
|
+
version='1.0.0',
|
|
280
|
+
description=f"Backend: {backend_class.__name__}",
|
|
281
|
+
capabilities=[],
|
|
282
|
+
author='',
|
|
283
|
+
requirements=[]
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def reload_backend(self, name: str) -> bool:
|
|
287
|
+
"""
|
|
288
|
+
Reload a backend module.
|
|
289
|
+
Args:
|
|
290
|
+
name: Backend key in format 'publisher:plugin' or class backend id
|
|
291
|
+
Returns:
|
|
292
|
+
True if successful
|
|
293
|
+
"""
|
|
294
|
+
search_name = name.lower()
|
|
295
|
+
# Check if using discovery path key
|
|
296
|
+
if search_name not in self._backend_paths:
|
|
297
|
+
return False
|
|
298
|
+
backend_dir = self._backend_paths[search_name]
|
|
299
|
+
# Extract publisher and plugin names from path
|
|
300
|
+
plugin_name = backend_dir.name
|
|
301
|
+
publisher_name = backend_dir.parent.name
|
|
302
|
+
module_name = f"backend_{publisher_name}_{plugin_name}"
|
|
303
|
+
try:
|
|
304
|
+
if search_name in self._backends:
|
|
305
|
+
del self._backends[search_name]
|
|
306
|
+
if search_name in self._metadata:
|
|
307
|
+
del self._metadata[search_name]
|
|
308
|
+
|
|
309
|
+
await self._load_backend_by_name(search_name)
|
|
310
|
+
return True
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Failed to reload {name}: {e}")
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
def unload_backend(self, name: str) -> bool:
|
|
316
|
+
"""
|
|
317
|
+
Unload a backend module from memory.
|
|
318
|
+
Args:
|
|
319
|
+
name: Backend key in format 'publisher:plugin' or class backend id
|
|
320
|
+
Returns:
|
|
321
|
+
True if successful
|
|
322
|
+
"""
|
|
323
|
+
search_name = name.lower()
|
|
324
|
+
# Check if in discovery path
|
|
325
|
+
if search_name not in self._backend_paths:
|
|
326
|
+
return False
|
|
327
|
+
backend_dir = self._backend_paths[search_name]
|
|
328
|
+
# Extract publisher and plugin names from path
|
|
329
|
+
plugin_name = backend_dir.name
|
|
330
|
+
publisher_name = backend_dir.parent.name
|
|
331
|
+
module_name = f"backend_{publisher_name}_{plugin_name}"
|
|
332
|
+
try:
|
|
333
|
+
# Remove from backends registry
|
|
334
|
+
if search_name in self._backends:
|
|
335
|
+
del self._backends[search_name]
|
|
336
|
+
# Remove from metadata
|
|
337
|
+
if search_name in self._metadata:
|
|
338
|
+
del self._metadata[search_name]
|
|
339
|
+
# Remove from discovery paths
|
|
340
|
+
if search_name in self._backend_paths:
|
|
341
|
+
del self._backend_paths[search_name]
|
|
342
|
+
# Remove from sys.modules
|
|
343
|
+
if module_name in sys.modules:
|
|
344
|
+
del sys.modules[module_name]
|
|
345
|
+
logger.info(f"Unloaded backend: {name}")
|
|
346
|
+
return True
|
|
347
|
+
except Exception as e:
|
|
348
|
+
logger.error(f"Failed to unload {name}: {e}")
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
def issubclass_safe(cls: Type, parent: Type) -> bool:
|
|
352
|
+
"""Safe issubclass check that handles import issues."""
|
|
353
|
+
try:
|
|
354
|
+
return issubclass(cls, parent)
|
|
355
|
+
except TypeError:
|
|
356
|
+
return False
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Backend Module
|
|
3
|
+
Provides the foundation for all backend implementations including:
|
|
4
|
+
- BackendMetadata dataclass for backend information
|
|
5
|
+
- BaseBackend abstract base class
|
|
6
|
+
"""
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Set, List
|
|
10
|
+
@dataclass
|
|
11
|
+
class BackendMetadata:
|
|
12
|
+
"""Metadata describing a backend."""
|
|
13
|
+
name: str
|
|
14
|
+
version: str
|
|
15
|
+
description: str
|
|
16
|
+
capabilities: List[str]
|
|
17
|
+
author: str = ""
|
|
18
|
+
requirements: List[str] = field(default_factory=list)
|
|
19
|
+
def to_dict(self) -> dict:
|
|
20
|
+
"""Convert to dictionary for serialization."""
|
|
21
|
+
return {
|
|
22
|
+
"name": self.name,
|
|
23
|
+
"version": self.version,
|
|
24
|
+
"description": self.description,
|
|
25
|
+
"capabilities": [cap for cap in self.capabilities],
|
|
26
|
+
"author": self.author,
|
|
27
|
+
"requirements": self.requirements
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class BaseBackend(ABC):
|
|
31
|
+
"""
|
|
32
|
+
Abstract base class for all backends.
|
|
33
|
+
All backend implementations must:
|
|
34
|
+
1. Set a unique `backend` class attribute
|
|
35
|
+
2. Implement the `metadata` property
|
|
36
|
+
"""
|
|
37
|
+
# Must be overridden by subclasses
|
|
38
|
+
backend: str = ""
|
|
39
|
+
use_lock: bool = False
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
return f"<{self.__class__.__name__} backend='{self.backend}'>"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Embedding Module
|
|
3
|
+
Abstract base class for Embedding backends.
|
|
4
|
+
"""
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from typing import List, Tuple, Optional, Union
|
|
7
|
+
import numpy as np
|
|
8
|
+
from .base_backend import BaseBackend
|
|
9
|
+
class BaseEmbedding(BaseBackend):
|
|
10
|
+
"""
|
|
11
|
+
Abstract base class for Embedding backends.
|
|
12
|
+
Provides text embedding and reranking capabilities.
|
|
13
|
+
"""
|
|
14
|
+
@property
|
|
15
|
+
def dimension(self) -> int:
|
|
16
|
+
"""Return the embedding dimension. Override if known at runtime."""
|
|
17
|
+
raise NotImplementedError("Subclass must implement dimension property")
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def embed(
|
|
21
|
+
self,
|
|
22
|
+
texts: Union[str, List[str]],
|
|
23
|
+
normalize: bool = True,
|
|
24
|
+
batch_size: int = 32,
|
|
25
|
+
**kwargs
|
|
26
|
+
) -> np.ndarray:
|
|
27
|
+
"""
|
|
28
|
+
Embed texts into vectors.
|
|
29
|
+
Args:
|
|
30
|
+
texts: Single text or list of texts
|
|
31
|
+
normalize: Whether to L2 normalize embeddings
|
|
32
|
+
batch_size: Batch size for processing
|
|
33
|
+
Returns:
|
|
34
|
+
numpy array of shape (n_texts, dimension)
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def create_embedding(
|
|
40
|
+
self,
|
|
41
|
+
texts: Union[str, List[str]],
|
|
42
|
+
task: Optional[str] = None,
|
|
43
|
+
normalize: bool = True,
|
|
44
|
+
batch_size: int = 32,
|
|
45
|
+
**kwargs
|
|
46
|
+
) -> np.ndarray:
|
|
47
|
+
"""
|
|
48
|
+
Create embeddings with optional task-specific prefixes.
|
|
49
|
+
Args:
|
|
50
|
+
texts: Single text or list of texts
|
|
51
|
+
task: Task type ('query', 'document', 'code', etc.)
|
|
52
|
+
normalize: Whether to L2 normalize
|
|
53
|
+
batch_size: Batch size for processing
|
|
54
|
+
Returns:
|
|
55
|
+
numpy array of embeddings
|
|
56
|
+
"""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def embed_query(
|
|
61
|
+
self,
|
|
62
|
+
query: str,
|
|
63
|
+
normalize: bool = True,
|
|
64
|
+
**kwargs
|
|
65
|
+
) -> np.ndarray:
|
|
66
|
+
"""
|
|
67
|
+
Embed a query for retrieval.
|
|
68
|
+
Args:
|
|
69
|
+
query: Query text
|
|
70
|
+
normalize: Whether to normalize
|
|
71
|
+
Returns:
|
|
72
|
+
1D numpy array of shape (dimension,)
|
|
73
|
+
"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def embed_documents(
|
|
78
|
+
self,
|
|
79
|
+
documents: List[str],
|
|
80
|
+
titles: Optional[List[str]] = None,
|
|
81
|
+
normalize: bool = True,
|
|
82
|
+
**kwargs
|
|
83
|
+
) -> np.ndarray:
|
|
84
|
+
"""
|
|
85
|
+
Embed documents for retrieval.
|
|
86
|
+
Args:
|
|
87
|
+
documents: List of document texts
|
|
88
|
+
titles: Optional list of document titles
|
|
89
|
+
normalize: Whether to normalize
|
|
90
|
+
Returns:
|
|
91
|
+
numpy array of shape (n_docs, dimension)
|
|
92
|
+
"""
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def rerank(
|
|
97
|
+
self,
|
|
98
|
+
query: str,
|
|
99
|
+
documents: List[str],
|
|
100
|
+
top_k: Optional[int] = None,
|
|
101
|
+
query_task: Optional[str] = None,
|
|
102
|
+
document_task: Optional[str] = None,
|
|
103
|
+
**kwargs
|
|
104
|
+
) -> List[Tuple[int, float, str]]:
|
|
105
|
+
"""
|
|
106
|
+
Rerank documents by relevance to query.
|
|
107
|
+
Args:
|
|
108
|
+
query: Query text
|
|
109
|
+
documents: List of documents to rerank
|
|
110
|
+
top_k: Return only top k results
|
|
111
|
+
query_task: Task prefix for query
|
|
112
|
+
document_task: Task prefix for documents
|
|
113
|
+
Returns:
|
|
114
|
+
List of (index, score, text) tuples sorted by score descending
|
|
115
|
+
"""
|
|
116
|
+
pass
|
openchadpy/base_lm.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base LLM Module
|
|
3
|
+
Abstract base class for Language Model backends.
|
|
4
|
+
"""
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from typing import Dict, Generator, Union, List, Any, Optional
|
|
7
|
+
from .base_backend import BaseBackend
|
|
8
|
+
class BaseLM(BaseBackend):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for LLM backends.
|
|
11
|
+
Provides text generation and chat completion capabilities.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def generate(
|
|
16
|
+
self,
|
|
17
|
+
prompt: str,
|
|
18
|
+
max_tokens: int = 4096,
|
|
19
|
+
temperature: float = 0.8,
|
|
20
|
+
top_p: float = 0.95,
|
|
21
|
+
stop: Optional[List[str]] = None,
|
|
22
|
+
stream: bool = False,
|
|
23
|
+
**kwargs
|
|
24
|
+
) -> Union[Dict, Generator]:
|
|
25
|
+
"""
|
|
26
|
+
Generate text completion based on a prompt.
|
|
27
|
+
Args:
|
|
28
|
+
prompt: The input prompt
|
|
29
|
+
max_tokens: Maximum tokens to generate
|
|
30
|
+
temperature: Sampling temperature
|
|
31
|
+
top_p: Top-p (nucleus) sampling
|
|
32
|
+
stop: Stop sequences
|
|
33
|
+
stream: Whether to stream the response
|
|
34
|
+
Returns:
|
|
35
|
+
Dict with completion or Generator for streaming
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def chat(
|
|
41
|
+
self,
|
|
42
|
+
messages: List[Dict[str, Any]],
|
|
43
|
+
max_tokens: int = 4096,
|
|
44
|
+
temperature: float = 0.8,
|
|
45
|
+
top_p: float = 0.95,
|
|
46
|
+
stream: bool = False,
|
|
47
|
+
**kwargs
|
|
48
|
+
) -> Union[Dict, Generator]:
|
|
49
|
+
"""
|
|
50
|
+
Generate chat completion from messages.
|
|
51
|
+
Args:
|
|
52
|
+
messages: List of message dicts with 'role' and 'content'
|
|
53
|
+
max_tokens: Maximum tokens to generate
|
|
54
|
+
temperature: Sampling temperature
|
|
55
|
+
top_p: Top-p (nucleus) sampling
|
|
56
|
+
stream: Whether to stream the response
|
|
57
|
+
Returns:
|
|
58
|
+
Dict with completion or Generator for streaming
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Model Provider Module
|
|
3
|
+
Provides the foundation for all model provider implementations.
|
|
4
|
+
"""
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .settings import Settings
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ProviderMetadata:
|
|
13
|
+
"""Metadata describing a model provider."""
|
|
14
|
+
name: str
|
|
15
|
+
version: str
|
|
16
|
+
description: str
|
|
17
|
+
author: str = ""
|
|
18
|
+
requirements: List[str] = field(default_factory=list)
|
|
19
|
+
def to_dict(self) -> dict:
|
|
20
|
+
"""Convert to dictionary for serialization."""
|
|
21
|
+
return {
|
|
22
|
+
"name": self.name,
|
|
23
|
+
"version": self.version,
|
|
24
|
+
"description": self.description,
|
|
25
|
+
"author": self.author,
|
|
26
|
+
"requirements": self.requirements
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class BaseModelProvider(ABC):
|
|
30
|
+
"""
|
|
31
|
+
Abstract base class for all model providers.
|
|
32
|
+
A model provider is responsible for scanning and discovering models
|
|
33
|
+
from a specific source (e.g., local files, Hugging Face, etc.).
|
|
34
|
+
"""
|
|
35
|
+
# Must be overridden by subclasses
|
|
36
|
+
provider_id: str = ""
|
|
37
|
+
settings_manager : Optional["Settings"]
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self.settings_manager = None
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def scan(self) -> List[Dict[str, Any]]:
|
|
43
|
+
"""
|
|
44
|
+
Scan for available models.
|
|
45
|
+
Returns:
|
|
46
|
+
A list of dictionaries, each representing a discovered model.
|
|
47
|
+
The dictionary should contain at least:
|
|
48
|
+
- id: Unique identifier for the model
|
|
49
|
+
- name: Human-readable name
|
|
50
|
+
- backend: The backend to use for this model
|
|
51
|
+
- model_type: Type of model (e.g., 'llm', 'embedding')
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
async def close(self):
|
|
56
|
+
"""
|
|
57
|
+
Cleanup resources (e.g., stop watchers, close connections).
|
|
58
|
+
Should be called when the provider is unloaded.
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
return f"<{self.__class__.__name__} provider_id='{self.provider_id}'>"
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def format_model_name(m_name):
|
|
66
|
+
words = str(m_name).split('/')[-1].replace('-', ' ').replace(':', ' ').replace('_', ' ').split()
|
|
67
|
+
result = []
|
|
68
|
+
for word in words:
|
|
69
|
+
letter_count = sum(c.isalpha() for c in word)
|
|
70
|
+
if letter_count <= 3:
|
|
71
|
+
result.append(word.upper())
|
|
72
|
+
else:
|
|
73
|
+
result.append(word.title())
|
|
74
|
+
return ' '.join(result)
|