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.
Files changed (47) hide show
  1. openchadpy/Tauri.toml +20 -0
  2. openchadpy/__init__.py +1 -0
  3. openchadpy/backend_registry.py +356 -0
  4. openchadpy/base_backend.py +41 -0
  5. openchadpy/base_embedding.py +116 -0
  6. openchadpy/base_lm.py +60 -0
  7. openchadpy/base_provider.py +74 -0
  8. openchadpy/base_stt.py +95 -0
  9. openchadpy/base_tts.py +46 -0
  10. openchadpy/base_vision.py +91 -0
  11. openchadpy/capabilities/default.json +52 -0
  12. openchadpy/connection_manager.py +64 -0
  13. openchadpy/context.py +8 -0
  14. openchadpy/credentials.py +150 -0
  15. openchadpy/database.py +258 -0
  16. openchadpy/database_manager.py +93 -0
  17. openchadpy/event_emitter.py +313 -0
  18. openchadpy/file.py +233 -0
  19. openchadpy/file_manager.py +236 -0
  20. openchadpy/icons/icon.ico +0 -0
  21. openchadpy/icons/icon.png +0 -0
  22. openchadpy/main.py +1987 -0
  23. openchadpy/mcp_manager.py +417 -0
  24. openchadpy/model_manager.py +1154 -0
  25. openchadpy/model_provider.py +291 -0
  26. openchadpy/pipeline_base.py +206 -0
  27. openchadpy/pipeline_manager.py +320 -0
  28. openchadpy/plugin_watcher.py +385 -0
  29. openchadpy/process_audio.py +95 -0
  30. openchadpy/process_image.py +86 -0
  31. openchadpy/process_video.py +129 -0
  32. openchadpy/proxy.py +82 -0
  33. openchadpy/settings.py +346 -0
  34. openchadpy/settings_subscription.py +48 -0
  35. openchadpy/sqlite.py +185 -0
  36. openchadpy/startup.py +68 -0
  37. openchadpy/streaming_response.py +151 -0
  38. openchadpy/tool_base.py +253 -0
  39. openchadpy/tool_manager.py +442 -0
  40. openchadpy/vram_checker.py +95 -0
  41. openchadpy/webrtc_client.py +188 -0
  42. openchadpy/websocket_client.py +29 -0
  43. openchadpy-0.1.0.dist-info/METADATA +37 -0
  44. openchadpy-0.1.0.dist-info/RECORD +47 -0
  45. openchadpy-0.1.0.dist-info/WHEEL +4 -0
  46. openchadpy-0.1.0.dist-info/entry_points.txt +2 -0
  47. 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)