abstractcore 2.5.3__py3-none-any.whl → 2.6.2__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.
- abstractcore/__init__.py +7 -1
- abstractcore/architectures/detection.py +2 -2
- abstractcore/config/__init__.py +24 -1
- abstractcore/config/manager.py +47 -0
- abstractcore/core/retry.py +2 -2
- abstractcore/core/session.py +132 -1
- abstractcore/download.py +253 -0
- abstractcore/embeddings/manager.py +2 -2
- abstractcore/events/__init__.py +112 -1
- abstractcore/exceptions/__init__.py +49 -2
- abstractcore/media/processors/office_processor.py +2 -2
- abstractcore/media/utils/image_scaler.py +2 -2
- abstractcore/media/vision_fallback.py +2 -2
- abstractcore/providers/anthropic_provider.py +200 -6
- abstractcore/providers/base.py +100 -5
- abstractcore/providers/lmstudio_provider.py +254 -4
- abstractcore/providers/ollama_provider.py +253 -4
- abstractcore/providers/openai_provider.py +258 -6
- abstractcore/providers/registry.py +9 -1
- abstractcore/providers/streaming.py +2 -2
- abstractcore/tools/common_tools.py +2 -2
- abstractcore/tools/handler.py +2 -2
- abstractcore/tools/parser.py +2 -2
- abstractcore/tools/registry.py +2 -2
- abstractcore/tools/syntax_rewriter.py +2 -2
- abstractcore/tools/tag_rewriter.py +3 -3
- abstractcore/utils/self_fixes.py +2 -2
- abstractcore/utils/version.py +1 -1
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/METADATA +162 -4
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/RECORD +34 -33
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/WHEEL +0 -0
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/entry_points.txt +0 -0
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/top_level.txt +0 -0
abstractcore/__init__.py
CHANGED
|
@@ -49,6 +49,9 @@ _has_processing = True
|
|
|
49
49
|
# Tools module (core functionality)
|
|
50
50
|
from .tools import tool
|
|
51
51
|
|
|
52
|
+
# Download module (core functionality)
|
|
53
|
+
from .download import download_model, DownloadProgress, DownloadStatus
|
|
54
|
+
|
|
52
55
|
# Compression module (optional import)
|
|
53
56
|
try:
|
|
54
57
|
from .compression import GlyphConfig, CompressionOrchestrator
|
|
@@ -67,7 +70,10 @@ __all__ = [
|
|
|
67
70
|
'ModelNotFoundError',
|
|
68
71
|
'ProviderAPIError',
|
|
69
72
|
'AuthenticationError',
|
|
70
|
-
'tool'
|
|
73
|
+
'tool',
|
|
74
|
+
'download_model',
|
|
75
|
+
'DownloadProgress',
|
|
76
|
+
'DownloadStatus',
|
|
71
77
|
]
|
|
72
78
|
|
|
73
79
|
if _has_embeddings:
|
|
@@ -9,9 +9,9 @@ import json
|
|
|
9
9
|
import os
|
|
10
10
|
from typing import Dict, Any, Optional, List
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
import
|
|
12
|
+
from ..utils.structured_logging import get_logger
|
|
13
13
|
|
|
14
|
-
logger =
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
15
|
|
|
16
16
|
# Cache for loaded JSON data
|
|
17
17
|
_architecture_formats: Optional[Dict[str, Any]] = None
|
abstractcore/config/__init__.py
CHANGED
|
@@ -7,4 +7,27 @@ Provides configuration management and command-line interface for AbstractCore.
|
|
|
7
7
|
from .vision_config import handle_vision_commands, add_vision_arguments
|
|
8
8
|
from .manager import get_config_manager
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
def configure_provider(provider: str, **kwargs) -> None:
|
|
12
|
+
"""Configure runtime settings for a provider."""
|
|
13
|
+
get_config_manager().configure_provider(provider, **kwargs)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_provider_config(provider: str) -> dict:
|
|
17
|
+
"""Get runtime configuration for a provider."""
|
|
18
|
+
return get_config_manager().get_provider_config(provider)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def clear_provider_config(provider: str = None) -> None:
|
|
22
|
+
"""Clear runtime provider configuration."""
|
|
23
|
+
get_config_manager().clear_provider_config(provider)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
'handle_vision_commands',
|
|
28
|
+
'add_vision_arguments',
|
|
29
|
+
'get_config_manager',
|
|
30
|
+
'configure_provider',
|
|
31
|
+
'get_provider_config',
|
|
32
|
+
'clear_provider_config'
|
|
33
|
+
]
|
abstractcore/config/manager.py
CHANGED
|
@@ -136,6 +136,7 @@ class ConfigurationManager:
|
|
|
136
136
|
self.config_dir = Path.home() / ".abstractcore" / "config"
|
|
137
137
|
self.config_file = self.config_dir / "abstractcore.json"
|
|
138
138
|
self.config = self._load_config()
|
|
139
|
+
self._provider_config: Dict[str, Dict[str, Any]] = {} # Runtime config (not persisted)
|
|
139
140
|
|
|
140
141
|
def _load_config(self) -> AbstractCoreConfig:
|
|
141
142
|
"""Load configuration from file or create default."""
|
|
@@ -437,6 +438,52 @@ class ConfigurationManager:
|
|
|
437
438
|
"""Check if local_files_only should be forced for transformers."""
|
|
438
439
|
return self.config.offline.force_local_files_only
|
|
439
440
|
|
|
441
|
+
def configure_provider(self, provider: str, **kwargs) -> None:
|
|
442
|
+
"""
|
|
443
|
+
Configure runtime settings for a provider.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
provider: Provider name ('ollama', 'lmstudio', 'openai', 'anthropic')
|
|
447
|
+
**kwargs: Configuration options (base_url, timeout, etc.)
|
|
448
|
+
|
|
449
|
+
Example:
|
|
450
|
+
configure_provider('ollama', base_url='http://192.168.1.100:11434')
|
|
451
|
+
"""
|
|
452
|
+
provider = provider.lower()
|
|
453
|
+
if provider not in self._provider_config:
|
|
454
|
+
self._provider_config[provider] = {}
|
|
455
|
+
|
|
456
|
+
for key, value in kwargs.items():
|
|
457
|
+
if value is None:
|
|
458
|
+
# Remove config (revert to env var / default)
|
|
459
|
+
self._provider_config[provider].pop(key, None)
|
|
460
|
+
else:
|
|
461
|
+
self._provider_config[provider][key] = value
|
|
462
|
+
|
|
463
|
+
def get_provider_config(self, provider: str) -> Dict[str, Any]:
|
|
464
|
+
"""
|
|
465
|
+
Get runtime configuration for a provider.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
provider: Provider name
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Dict with configured settings, or empty dict if no config
|
|
472
|
+
"""
|
|
473
|
+
return self._provider_config.get(provider.lower(), {}).copy()
|
|
474
|
+
|
|
475
|
+
def clear_provider_config(self, provider: Optional[str] = None) -> None:
|
|
476
|
+
"""
|
|
477
|
+
Clear runtime provider configuration.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
provider: Provider name, or None to clear all
|
|
481
|
+
"""
|
|
482
|
+
if provider is None:
|
|
483
|
+
self._provider_config.clear()
|
|
484
|
+
else:
|
|
485
|
+
self._provider_config.pop(provider.lower(), None)
|
|
486
|
+
|
|
440
487
|
|
|
441
488
|
# Global instance
|
|
442
489
|
_config_manager = None
|
abstractcore/core/retry.py
CHANGED
|
@@ -8,13 +8,13 @@ and production LLM system requirements.
|
|
|
8
8
|
|
|
9
9
|
import time
|
|
10
10
|
import random
|
|
11
|
-
import logging
|
|
12
11
|
from typing import Type, Optional, Set, Dict, Any
|
|
13
12
|
from dataclasses import dataclass
|
|
14
13
|
from datetime import datetime, timedelta
|
|
15
14
|
from enum import Enum
|
|
15
|
+
from ..utils.structured_logging import get_logger
|
|
16
16
|
|
|
17
|
-
logger =
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class RetryableErrorType(Enum):
|
abstractcore/core/session.py
CHANGED
|
@@ -3,11 +3,12 @@ BasicSession for conversation tracking.
|
|
|
3
3
|
Target: <500 lines maximum.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import List, Optional, Dict, Any, Union, Iterator, Callable
|
|
6
|
+
from typing import List, Optional, Dict, Any, Union, Iterator, AsyncIterator, Callable
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
import json
|
|
10
10
|
import uuid
|
|
11
|
+
import asyncio
|
|
11
12
|
from collections.abc import Generator
|
|
12
13
|
|
|
13
14
|
from .interface import AbstractCoreInterface
|
|
@@ -273,6 +274,136 @@ class BasicSession:
|
|
|
273
274
|
if collected_content:
|
|
274
275
|
self.add_message('assistant', collected_content)
|
|
275
276
|
|
|
277
|
+
async def agenerate(self,
|
|
278
|
+
prompt: str,
|
|
279
|
+
name: Optional[str] = None,
|
|
280
|
+
location: Optional[str] = None,
|
|
281
|
+
**kwargs) -> Union[GenerateResponse, AsyncIterator[GenerateResponse]]:
|
|
282
|
+
"""
|
|
283
|
+
Async generation with conversation history.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
prompt: User message
|
|
287
|
+
name: Optional speaker name
|
|
288
|
+
location: Optional location context
|
|
289
|
+
**kwargs: Generation parameters (stream, temperature, etc.)
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
GenerateResponse or AsyncIterator for streaming
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
# Async chat interaction
|
|
296
|
+
response = await session.agenerate('What is Python?')
|
|
297
|
+
|
|
298
|
+
# Async streaming
|
|
299
|
+
async for chunk in await session.agenerate('Tell me a story', stream=True):
|
|
300
|
+
print(chunk.content, end='')
|
|
301
|
+
"""
|
|
302
|
+
if not self.provider:
|
|
303
|
+
raise ValueError("No provider configured")
|
|
304
|
+
|
|
305
|
+
# Check for auto-compaction before generating
|
|
306
|
+
if self.auto_compact and self.should_compact(self.auto_compact_threshold):
|
|
307
|
+
print(f"🗜️ Auto-compacting session (tokens: {self.get_token_estimate()} > {self.auto_compact_threshold})")
|
|
308
|
+
compacted = self.compact(reason="auto_threshold")
|
|
309
|
+
# Replace current session with compacted version
|
|
310
|
+
self._replace_with_compacted(compacted)
|
|
311
|
+
|
|
312
|
+
# Pre-processing (fast, sync is fine)
|
|
313
|
+
self.add_message('user', prompt, name=name, location=location)
|
|
314
|
+
|
|
315
|
+
# Format messages for provider (exclude the current user message since provider will add it)
|
|
316
|
+
messages = self._format_messages_for_provider_excluding_current()
|
|
317
|
+
|
|
318
|
+
# Use session tools if not provided in kwargs
|
|
319
|
+
if 'tools' not in kwargs and self.tools:
|
|
320
|
+
kwargs['tools'] = self.tools
|
|
321
|
+
|
|
322
|
+
# Pass session tool_call_tags if available and not overridden in kwargs
|
|
323
|
+
if hasattr(self, 'tool_call_tags') and self.tool_call_tags is not None and 'tool_call_tags' not in kwargs:
|
|
324
|
+
kwargs['tool_call_tags'] = self.tool_call_tags
|
|
325
|
+
|
|
326
|
+
# Extract media parameter explicitly
|
|
327
|
+
media = kwargs.pop('media', None)
|
|
328
|
+
|
|
329
|
+
# Add session-level parameters if not overridden in kwargs
|
|
330
|
+
if 'temperature' not in kwargs and self.temperature is not None:
|
|
331
|
+
kwargs['temperature'] = self.temperature
|
|
332
|
+
if 'seed' not in kwargs and self.seed is not None:
|
|
333
|
+
kwargs['seed'] = self.seed
|
|
334
|
+
|
|
335
|
+
# Add trace metadata if tracing is enabled
|
|
336
|
+
if self.enable_tracing:
|
|
337
|
+
if 'trace_metadata' not in kwargs:
|
|
338
|
+
kwargs['trace_metadata'] = {}
|
|
339
|
+
kwargs['trace_metadata'].update({
|
|
340
|
+
'session_id': self.id,
|
|
341
|
+
'step_type': kwargs.get('step_type', 'chat'),
|
|
342
|
+
'attempt_number': kwargs.get('attempt_number', 1)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
# Check if streaming
|
|
346
|
+
stream = kwargs.get('stream', False)
|
|
347
|
+
|
|
348
|
+
if stream:
|
|
349
|
+
# Return async streaming wrapper that adds assistant message after
|
|
350
|
+
return self._async_session_stream(prompt, messages, media, **kwargs)
|
|
351
|
+
else:
|
|
352
|
+
# Async generation
|
|
353
|
+
response = await self.provider.agenerate(
|
|
354
|
+
prompt=prompt,
|
|
355
|
+
messages=messages,
|
|
356
|
+
system_prompt=self.system_prompt,
|
|
357
|
+
media=media,
|
|
358
|
+
**kwargs
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Post-processing (fast, sync is fine)
|
|
362
|
+
if hasattr(response, 'content') and response.content:
|
|
363
|
+
self.add_message('assistant', response.content)
|
|
364
|
+
|
|
365
|
+
# Capture trace if enabled and available
|
|
366
|
+
if self.enable_tracing and hasattr(self.provider, 'get_traces'):
|
|
367
|
+
if hasattr(response, 'metadata') and response.metadata and 'trace_id' in response.metadata:
|
|
368
|
+
trace = self.provider.get_traces(response.metadata['trace_id'])
|
|
369
|
+
if trace:
|
|
370
|
+
self.interaction_traces.append(trace)
|
|
371
|
+
|
|
372
|
+
return response
|
|
373
|
+
|
|
374
|
+
async def _async_session_stream(self,
|
|
375
|
+
prompt: str,
|
|
376
|
+
messages: List[Dict[str, str]],
|
|
377
|
+
media: Optional[List],
|
|
378
|
+
**kwargs) -> AsyncIterator[GenerateResponse]:
|
|
379
|
+
"""Async streaming with session history management."""
|
|
380
|
+
collected_content = ""
|
|
381
|
+
|
|
382
|
+
# Remove 'stream' from kwargs since we're explicitly setting it
|
|
383
|
+
kwargs_copy = {k: v for k, v in kwargs.items() if k != 'stream'}
|
|
384
|
+
|
|
385
|
+
# CRITICAL: Await first to get async generator, then iterate
|
|
386
|
+
stream_gen = await self.provider.agenerate(
|
|
387
|
+
prompt=prompt,
|
|
388
|
+
messages=messages,
|
|
389
|
+
system_prompt=self.system_prompt,
|
|
390
|
+
media=media,
|
|
391
|
+
stream=True,
|
|
392
|
+
**kwargs_copy
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
async for chunk in stream_gen:
|
|
396
|
+
# Yield the chunk for the caller
|
|
397
|
+
yield chunk
|
|
398
|
+
|
|
399
|
+
# Collect content for history
|
|
400
|
+
if hasattr(chunk, 'content') and chunk.content:
|
|
401
|
+
collected_content += chunk.content
|
|
402
|
+
|
|
403
|
+
# After streaming completes, add assistant message
|
|
404
|
+
if collected_content:
|
|
405
|
+
self.add_message('assistant', collected_content)
|
|
406
|
+
|
|
276
407
|
def _format_messages_for_provider(self) -> List[Dict[str, str]]:
|
|
277
408
|
"""Format messages for provider API"""
|
|
278
409
|
return [
|
abstractcore/download.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model download API with async progress reporting.
|
|
3
|
+
|
|
4
|
+
Provides a provider-agnostic interface for downloading models from Ollama,
|
|
5
|
+
HuggingFace Hub, and MLX with streaming progress updates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import asyncio
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import AsyncIterator, Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DownloadStatus(Enum):
|
|
18
|
+
"""Download progress status."""
|
|
19
|
+
|
|
20
|
+
STARTING = "starting"
|
|
21
|
+
DOWNLOADING = "downloading"
|
|
22
|
+
VERIFYING = "verifying"
|
|
23
|
+
COMPLETE = "complete"
|
|
24
|
+
ERROR = "error"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class DownloadProgress:
|
|
29
|
+
"""Progress information for model download."""
|
|
30
|
+
|
|
31
|
+
status: DownloadStatus
|
|
32
|
+
message: str
|
|
33
|
+
percent: Optional[float] = None # 0-100
|
|
34
|
+
downloaded_bytes: Optional[int] = None
|
|
35
|
+
total_bytes: Optional[int] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def download_model(
|
|
39
|
+
provider: str,
|
|
40
|
+
model: str,
|
|
41
|
+
token: Optional[str] = None,
|
|
42
|
+
base_url: Optional[str] = None,
|
|
43
|
+
) -> AsyncIterator[DownloadProgress]:
|
|
44
|
+
"""
|
|
45
|
+
Download a model with async progress reporting.
|
|
46
|
+
|
|
47
|
+
This function provides a unified interface for downloading models across
|
|
48
|
+
different providers. Progress updates are yielded as DownloadProgress
|
|
49
|
+
dataclasses that include status, message, and optional progress percentage.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
provider: Provider name ("ollama", "huggingface", "mlx")
|
|
53
|
+
model: Model identifier:
|
|
54
|
+
- Ollama: "llama3:8b", "gemma3:1b", etc.
|
|
55
|
+
- HuggingFace/MLX: "meta-llama/Llama-2-7b", "mlx-community/Qwen3-4B-4bit", etc.
|
|
56
|
+
token: Optional auth token (for HuggingFace gated models)
|
|
57
|
+
base_url: Optional custom base URL (for Ollama, default: http://localhost:11434)
|
|
58
|
+
|
|
59
|
+
Yields:
|
|
60
|
+
DownloadProgress: Progress updates with status, message, and optional metrics
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If provider doesn't support downloads (OpenAI, Anthropic, LMStudio)
|
|
64
|
+
httpx.HTTPStatusError: If Ollama server returns error
|
|
65
|
+
Exception: Various exceptions from HuggingFace Hub (RepositoryNotFoundError, etc.)
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
Download Ollama model:
|
|
69
|
+
>>> async for progress in download_model("ollama", "gemma3:1b"):
|
|
70
|
+
... print(f"{progress.status.value}: {progress.message}")
|
|
71
|
+
... if progress.percent:
|
|
72
|
+
... print(f" Progress: {progress.percent:.1f}%")
|
|
73
|
+
|
|
74
|
+
Download HuggingFace model with token:
|
|
75
|
+
>>> async for progress in download_model(
|
|
76
|
+
... "huggingface",
|
|
77
|
+
... "meta-llama/Llama-2-7b",
|
|
78
|
+
... token="hf_..."
|
|
79
|
+
... ):
|
|
80
|
+
... print(f"{progress.message}")
|
|
81
|
+
"""
|
|
82
|
+
provider_lower = provider.lower()
|
|
83
|
+
|
|
84
|
+
if provider_lower == "ollama":
|
|
85
|
+
async for progress in _download_ollama(model, base_url):
|
|
86
|
+
yield progress
|
|
87
|
+
elif provider_lower in ("huggingface", "mlx"):
|
|
88
|
+
async for progress in _download_huggingface(model, token):
|
|
89
|
+
yield progress
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Provider '{provider}' does not support model downloads. "
|
|
93
|
+
f"Supported providers: ollama, huggingface, mlx. "
|
|
94
|
+
f"Note: OpenAI and Anthropic are cloud-only; LMStudio has no download API."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _download_ollama(
|
|
99
|
+
model: str,
|
|
100
|
+
base_url: Optional[str] = None,
|
|
101
|
+
) -> AsyncIterator[DownloadProgress]:
|
|
102
|
+
"""
|
|
103
|
+
Download model from Ollama using /api/pull endpoint.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
model: Ollama model name (e.g., "llama3:8b", "gemma3:1b")
|
|
107
|
+
base_url: Ollama server URL (default: http://localhost:11434)
|
|
108
|
+
|
|
109
|
+
Yields:
|
|
110
|
+
DownloadProgress with status updates from Ollama streaming response
|
|
111
|
+
"""
|
|
112
|
+
url = (base_url or "http://localhost:11434").rstrip("/")
|
|
113
|
+
|
|
114
|
+
yield DownloadProgress(
|
|
115
|
+
status=DownloadStatus.STARTING, message=f"Pulling {model} from Ollama..."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
120
|
+
async with client.stream(
|
|
121
|
+
"POST",
|
|
122
|
+
f"{url}/api/pull",
|
|
123
|
+
json={"name": model, "stream": True},
|
|
124
|
+
) as response:
|
|
125
|
+
response.raise_for_status()
|
|
126
|
+
|
|
127
|
+
async for line in response.aiter_lines():
|
|
128
|
+
if not line:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
data = json.loads(line)
|
|
133
|
+
except json.JSONDecodeError:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
status_msg = data.get("status", "")
|
|
137
|
+
|
|
138
|
+
# Parse progress from Ollama response
|
|
139
|
+
# Format: {"status": "downloading...", "total": 123, "completed": 45}
|
|
140
|
+
if "total" in data and "completed" in data:
|
|
141
|
+
total = data["total"]
|
|
142
|
+
completed = data["completed"]
|
|
143
|
+
percent = (completed / total * 100) if total > 0 else 0
|
|
144
|
+
|
|
145
|
+
yield DownloadProgress(
|
|
146
|
+
status=DownloadStatus.DOWNLOADING,
|
|
147
|
+
message=status_msg,
|
|
148
|
+
percent=percent,
|
|
149
|
+
downloaded_bytes=completed,
|
|
150
|
+
total_bytes=total,
|
|
151
|
+
)
|
|
152
|
+
elif status_msg == "success":
|
|
153
|
+
yield DownloadProgress(
|
|
154
|
+
status=DownloadStatus.COMPLETE,
|
|
155
|
+
message=f"Successfully pulled {model}",
|
|
156
|
+
percent=100.0,
|
|
157
|
+
)
|
|
158
|
+
elif "verifying" in status_msg.lower():
|
|
159
|
+
yield DownloadProgress(
|
|
160
|
+
status=DownloadStatus.VERIFYING,
|
|
161
|
+
message=status_msg,
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
# Other status messages (pulling manifest, etc.)
|
|
165
|
+
yield DownloadProgress(
|
|
166
|
+
status=DownloadStatus.DOWNLOADING,
|
|
167
|
+
message=status_msg,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
except httpx.HTTPStatusError as e:
|
|
171
|
+
yield DownloadProgress(
|
|
172
|
+
status=DownloadStatus.ERROR,
|
|
173
|
+
message=f"Ollama server error: {e.response.status_code} - {e.response.text}",
|
|
174
|
+
)
|
|
175
|
+
except httpx.ConnectError:
|
|
176
|
+
yield DownloadProgress(
|
|
177
|
+
status=DownloadStatus.ERROR,
|
|
178
|
+
message=f"Cannot connect to Ollama server at {url}. Is Ollama running?",
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
yield DownloadProgress(
|
|
182
|
+
status=DownloadStatus.ERROR,
|
|
183
|
+
message=f"Download failed: {str(e)}",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def _download_huggingface(
|
|
188
|
+
model: str,
|
|
189
|
+
token: Optional[str] = None,
|
|
190
|
+
) -> AsyncIterator[DownloadProgress]:
|
|
191
|
+
"""
|
|
192
|
+
Download model from HuggingFace Hub.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
model: HuggingFace model identifier (e.g., "meta-llama/Llama-2-7b")
|
|
196
|
+
token: Optional HuggingFace token (required for gated models)
|
|
197
|
+
|
|
198
|
+
Yields:
|
|
199
|
+
DownloadProgress with status updates
|
|
200
|
+
"""
|
|
201
|
+
yield DownloadProgress(
|
|
202
|
+
status=DownloadStatus.STARTING,
|
|
203
|
+
message=f"Downloading {model} from HuggingFace Hub...",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
# Import here to make huggingface_hub optional
|
|
208
|
+
from huggingface_hub import snapshot_download
|
|
209
|
+
from huggingface_hub.utils import GatedRepoError, RepositoryNotFoundError
|
|
210
|
+
except ImportError:
|
|
211
|
+
yield DownloadProgress(
|
|
212
|
+
status=DownloadStatus.ERROR,
|
|
213
|
+
message=(
|
|
214
|
+
"huggingface_hub is not installed. "
|
|
215
|
+
"Install with: pip install abstractcore[huggingface]"
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
# Run blocking download in thread
|
|
222
|
+
# Note: snapshot_download doesn't have built-in async progress callbacks
|
|
223
|
+
# We provide start and completion messages
|
|
224
|
+
await asyncio.to_thread(
|
|
225
|
+
snapshot_download,
|
|
226
|
+
repo_id=model,
|
|
227
|
+
token=token,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
yield DownloadProgress(
|
|
231
|
+
status=DownloadStatus.COMPLETE,
|
|
232
|
+
message=f"Successfully downloaded {model}",
|
|
233
|
+
percent=100.0,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
except RepositoryNotFoundError:
|
|
237
|
+
yield DownloadProgress(
|
|
238
|
+
status=DownloadStatus.ERROR,
|
|
239
|
+
message=f"Model '{model}' not found on HuggingFace Hub",
|
|
240
|
+
)
|
|
241
|
+
except GatedRepoError:
|
|
242
|
+
yield DownloadProgress(
|
|
243
|
+
status=DownloadStatus.ERROR,
|
|
244
|
+
message=(
|
|
245
|
+
f"Model '{model}' requires authentication. "
|
|
246
|
+
f"Provide a HuggingFace token via the 'token' parameter."
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
yield DownloadProgress(
|
|
251
|
+
status=DownloadStatus.ERROR,
|
|
252
|
+
message=f"Download failed: {str(e)}",
|
|
253
|
+
)
|
|
@@ -7,7 +7,6 @@ Production-ready embedding generation with SOTA models and efficient serving.
|
|
|
7
7
|
|
|
8
8
|
import hashlib
|
|
9
9
|
import pickle
|
|
10
|
-
import logging
|
|
11
10
|
import atexit
|
|
12
11
|
import sys
|
|
13
12
|
import builtins
|
|
@@ -33,8 +32,9 @@ except ImportError:
|
|
|
33
32
|
emit_global = None
|
|
34
33
|
|
|
35
34
|
from .models import EmbeddingBackend, get_model_config, list_available_models, get_default_model
|
|
35
|
+
from ..utils.structured_logging import get_logger
|
|
36
36
|
|
|
37
|
-
logger =
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
@contextmanager
|