abstractcore 2.5.2__py3-none-any.whl → 2.6.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.
- abstractcore/__init__.py +19 -1
- abstractcore/architectures/detection.py +252 -6
- abstractcore/assets/architecture_formats.json +14 -1
- abstractcore/assets/model_capabilities.json +533 -10
- abstractcore/compression/__init__.py +29 -0
- abstractcore/compression/analytics.py +420 -0
- abstractcore/compression/cache.py +250 -0
- abstractcore/compression/config.py +279 -0
- abstractcore/compression/exceptions.py +30 -0
- abstractcore/compression/glyph_processor.py +381 -0
- abstractcore/compression/optimizer.py +388 -0
- abstractcore/compression/orchestrator.py +380 -0
- abstractcore/compression/pil_text_renderer.py +818 -0
- abstractcore/compression/quality.py +226 -0
- abstractcore/compression/text_formatter.py +666 -0
- abstractcore/compression/vision_compressor.py +371 -0
- abstractcore/config/main.py +64 -0
- abstractcore/config/manager.py +100 -5
- abstractcore/core/retry.py +2 -2
- abstractcore/core/session.py +193 -7
- abstractcore/download.py +253 -0
- abstractcore/embeddings/manager.py +2 -2
- abstractcore/events/__init__.py +113 -2
- abstractcore/exceptions/__init__.py +49 -2
- abstractcore/media/auto_handler.py +312 -18
- abstractcore/media/handlers/local_handler.py +14 -2
- abstractcore/media/handlers/openai_handler.py +62 -3
- abstractcore/media/processors/__init__.py +11 -1
- abstractcore/media/processors/direct_pdf_processor.py +210 -0
- abstractcore/media/processors/glyph_pdf_processor.py +227 -0
- abstractcore/media/processors/image_processor.py +7 -1
- abstractcore/media/processors/office_processor.py +2 -2
- abstractcore/media/processors/text_processor.py +18 -3
- abstractcore/media/types.py +164 -7
- abstractcore/media/utils/image_scaler.py +2 -2
- abstractcore/media/vision_fallback.py +2 -2
- abstractcore/providers/__init__.py +18 -0
- abstractcore/providers/anthropic_provider.py +228 -8
- abstractcore/providers/base.py +378 -11
- abstractcore/providers/huggingface_provider.py +563 -23
- abstractcore/providers/lmstudio_provider.py +284 -4
- abstractcore/providers/mlx_provider.py +27 -2
- abstractcore/providers/model_capabilities.py +352 -0
- abstractcore/providers/ollama_provider.py +282 -6
- abstractcore/providers/openai_provider.py +286 -8
- abstractcore/providers/registry.py +85 -13
- abstractcore/providers/streaming.py +2 -2
- abstractcore/server/app.py +91 -81
- 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/__init__.py +4 -1
- abstractcore/utils/self_fixes.py +2 -2
- abstractcore/utils/trace_export.py +287 -0
- abstractcore/utils/version.py +1 -1
- abstractcore/utils/vlm_token_calculator.py +655 -0
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/METADATA +207 -8
- abstractcore-2.6.0.dist-info/RECORD +108 -0
- abstractcore-2.5.2.dist-info/RECORD +0 -90
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/WHEEL +0 -0
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/entry_points.txt +0 -0
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.5.2.dist-info → abstractcore-2.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vision-based compression module for enhanced Glyph compression.
|
|
3
|
+
|
|
4
|
+
This module simulates DeepSeek-OCR-like compression by using available
|
|
5
|
+
vision models to further compress Glyph-rendered images.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import base64
|
|
13
|
+
import hashlib
|
|
14
|
+
|
|
15
|
+
from ..utils.structured_logging import get_logger
|
|
16
|
+
from ..media.types import MediaContent, MediaType, ContentFormat
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class VisionCompressionResult:
|
|
21
|
+
"""Result from vision-based compression."""
|
|
22
|
+
original_images: int
|
|
23
|
+
compressed_tokens: int
|
|
24
|
+
compression_ratio: float
|
|
25
|
+
quality_score: float
|
|
26
|
+
processing_time: float
|
|
27
|
+
method: str
|
|
28
|
+
metadata: Dict[str, Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VisionCompressor:
|
|
32
|
+
"""
|
|
33
|
+
Vision-based compressor that further compresses Glyph-rendered images.
|
|
34
|
+
|
|
35
|
+
This simulates DeepSeek-OCR's approach by using vision models to
|
|
36
|
+
create compressed representations of text-heavy images.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, provider: str = "ollama", model: str = "llama3.2-vision"):
|
|
40
|
+
"""
|
|
41
|
+
Initialize vision compressor.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
provider: Vision model provider to use
|
|
45
|
+
model: Vision model for compression
|
|
46
|
+
"""
|
|
47
|
+
self.logger = get_logger(self.__class__.__name__)
|
|
48
|
+
self.provider = provider
|
|
49
|
+
self.model = model
|
|
50
|
+
|
|
51
|
+
# Compression configurations based on quality targets
|
|
52
|
+
self.compression_modes = {
|
|
53
|
+
"conservative": {
|
|
54
|
+
"target_ratio": 2.0,
|
|
55
|
+
"quality_threshold": 0.95,
|
|
56
|
+
"description": "Minimal compression, high quality"
|
|
57
|
+
},
|
|
58
|
+
"balanced": {
|
|
59
|
+
"target_ratio": 5.0,
|
|
60
|
+
"quality_threshold": 0.90,
|
|
61
|
+
"description": "Balanced compression and quality"
|
|
62
|
+
},
|
|
63
|
+
"aggressive": {
|
|
64
|
+
"target_ratio": 10.0,
|
|
65
|
+
"quality_threshold": 0.85,
|
|
66
|
+
"description": "Maximum compression, acceptable quality"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def compress_images(
|
|
71
|
+
self,
|
|
72
|
+
glyph_images: List[MediaContent],
|
|
73
|
+
mode: str = "balanced",
|
|
74
|
+
original_tokens: int = None
|
|
75
|
+
) -> VisionCompressionResult:
|
|
76
|
+
"""
|
|
77
|
+
Compress Glyph-rendered images using vision model.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
glyph_images: List of Glyph-rendered images
|
|
81
|
+
mode: Compression mode (conservative/balanced/aggressive)
|
|
82
|
+
original_tokens: Original token count before Glyph compression
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
VisionCompressionResult with compression metrics
|
|
86
|
+
"""
|
|
87
|
+
start_time = time.time()
|
|
88
|
+
|
|
89
|
+
if mode not in self.compression_modes:
|
|
90
|
+
raise ValueError(f"Invalid mode: {mode}. Use: {list(self.compression_modes.keys())}")
|
|
91
|
+
|
|
92
|
+
config = self.compression_modes[mode]
|
|
93
|
+
|
|
94
|
+
self.logger.info(f"Starting vision compression: {len(glyph_images)} images, mode={mode}")
|
|
95
|
+
|
|
96
|
+
# Simulate vision-based compression
|
|
97
|
+
# In a real implementation, this would:
|
|
98
|
+
# 1. Pass images through a vision encoder (like DeepSeek's SAM+CLIP)
|
|
99
|
+
# 2. Apply learned compression to reduce token count
|
|
100
|
+
# 3. Return compressed vision tokens
|
|
101
|
+
|
|
102
|
+
# For simulation, we calculate compressed tokens based on target ratio
|
|
103
|
+
original_image_tokens = len(glyph_images) * 1500 # Approximate tokens per image
|
|
104
|
+
|
|
105
|
+
# Apply vision compression based on mode
|
|
106
|
+
if mode == "conservative":
|
|
107
|
+
# Minimal compression - combine adjacent images
|
|
108
|
+
compressed_tokens = original_image_tokens // config["target_ratio"]
|
|
109
|
+
quality_score = 0.95
|
|
110
|
+
|
|
111
|
+
elif mode == "balanced":
|
|
112
|
+
# Balanced - more aggressive merging and compression
|
|
113
|
+
compressed_tokens = original_image_tokens // config["target_ratio"]
|
|
114
|
+
quality_score = 0.92
|
|
115
|
+
|
|
116
|
+
else: # aggressive
|
|
117
|
+
# Maximum compression - extreme token reduction
|
|
118
|
+
compressed_tokens = original_image_tokens // config["target_ratio"]
|
|
119
|
+
quality_score = 0.88
|
|
120
|
+
|
|
121
|
+
# Calculate overall compression ratio
|
|
122
|
+
if original_tokens:
|
|
123
|
+
overall_ratio = original_tokens / compressed_tokens
|
|
124
|
+
else:
|
|
125
|
+
overall_ratio = original_image_tokens / compressed_tokens
|
|
126
|
+
|
|
127
|
+
processing_time = time.time() - start_time
|
|
128
|
+
|
|
129
|
+
# Create result
|
|
130
|
+
result = VisionCompressionResult(
|
|
131
|
+
original_images=len(glyph_images),
|
|
132
|
+
compressed_tokens=int(compressed_tokens),
|
|
133
|
+
compression_ratio=overall_ratio,
|
|
134
|
+
quality_score=quality_score,
|
|
135
|
+
processing_time=processing_time,
|
|
136
|
+
method=f"vision_{mode}",
|
|
137
|
+
metadata={
|
|
138
|
+
"provider": self.provider,
|
|
139
|
+
"model": self.model,
|
|
140
|
+
"mode": mode,
|
|
141
|
+
"target_ratio": config["target_ratio"],
|
|
142
|
+
"quality_threshold": config["quality_threshold"]
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.logger.info(
|
|
147
|
+
f"Vision compression complete: {overall_ratio:.1f}x ratio, "
|
|
148
|
+
f"{quality_score:.2%} quality, {compressed_tokens} tokens"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
def compress_with_ocr(
|
|
154
|
+
self,
|
|
155
|
+
glyph_images: List[MediaContent],
|
|
156
|
+
extract_text: bool = True
|
|
157
|
+
) -> Tuple[VisionCompressionResult, Optional[str]]:
|
|
158
|
+
"""
|
|
159
|
+
Compress images and optionally extract text (OCR).
|
|
160
|
+
|
|
161
|
+
This simulates DeepSeek-OCR's dual capability:
|
|
162
|
+
1. Compress images to vision tokens
|
|
163
|
+
2. Extract text from images
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
glyph_images: List of Glyph-rendered images
|
|
167
|
+
extract_text: Whether to extract text from images
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Tuple of (compression result, extracted text)
|
|
171
|
+
"""
|
|
172
|
+
# Compress images
|
|
173
|
+
result = self.compress_images(glyph_images, mode="balanced")
|
|
174
|
+
|
|
175
|
+
extracted_text = None
|
|
176
|
+
if extract_text:
|
|
177
|
+
# In a real implementation, this would use OCR
|
|
178
|
+
# For now, we'll indicate that text extraction is possible
|
|
179
|
+
extracted_text = "[Text extraction would occur here with real OCR model]"
|
|
180
|
+
self.logger.info("Text extraction completed (simulated)")
|
|
181
|
+
|
|
182
|
+
return result, extracted_text
|
|
183
|
+
|
|
184
|
+
def adaptive_compress(
|
|
185
|
+
self,
|
|
186
|
+
glyph_images: List[MediaContent],
|
|
187
|
+
original_tokens: int,
|
|
188
|
+
target_ratio: float = 30.0,
|
|
189
|
+
min_quality: float = 0.85
|
|
190
|
+
) -> VisionCompressionResult:
|
|
191
|
+
"""
|
|
192
|
+
Adaptively compress to achieve target compression ratio.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
glyph_images: List of Glyph-rendered images
|
|
196
|
+
original_tokens: Original token count
|
|
197
|
+
target_ratio: Target compression ratio
|
|
198
|
+
min_quality: Minimum acceptable quality
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Best compression result achieving targets
|
|
202
|
+
"""
|
|
203
|
+
self.logger.info(f"Adaptive compression: target ratio={target_ratio:.1f}x, min quality={min_quality:.2%}")
|
|
204
|
+
|
|
205
|
+
# Try different modes to find best fit
|
|
206
|
+
best_result = None
|
|
207
|
+
|
|
208
|
+
for mode in ["aggressive", "balanced", "conservative"]:
|
|
209
|
+
result = self.compress_images(glyph_images, mode, original_tokens)
|
|
210
|
+
|
|
211
|
+
# Check if this meets our criteria
|
|
212
|
+
if result.quality_score >= min_quality:
|
|
213
|
+
if best_result is None or result.compression_ratio > best_result.compression_ratio:
|
|
214
|
+
best_result = result
|
|
215
|
+
|
|
216
|
+
# If we've achieved target, stop
|
|
217
|
+
if result.compression_ratio >= target_ratio:
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
if best_result is None:
|
|
221
|
+
# Fall back to conservative if nothing meets quality threshold
|
|
222
|
+
best_result = self.compress_images(glyph_images, "conservative", original_tokens)
|
|
223
|
+
|
|
224
|
+
self.logger.info(
|
|
225
|
+
f"Adaptive compression selected: {best_result.method}, "
|
|
226
|
+
f"{best_result.compression_ratio:.1f}x ratio, {best_result.quality_score:.2%} quality"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return best_result
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class HybridCompressionPipeline:
|
|
233
|
+
"""
|
|
234
|
+
Hybrid compression pipeline combining Glyph and vision compression.
|
|
235
|
+
|
|
236
|
+
This implements the theoretical Glyph → Vision Compressor pipeline
|
|
237
|
+
to achieve higher compression ratios.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __init__(
|
|
241
|
+
self,
|
|
242
|
+
vision_provider: str = "ollama",
|
|
243
|
+
vision_model: str = "llama3.2-vision"
|
|
244
|
+
):
|
|
245
|
+
"""
|
|
246
|
+
Initialize hybrid compression pipeline.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
vision_provider: Provider for vision compression
|
|
250
|
+
vision_model: Model for vision compression
|
|
251
|
+
"""
|
|
252
|
+
self.logger = get_logger(self.__class__.__name__)
|
|
253
|
+
self.vision_compressor = VisionCompressor(vision_provider, vision_model)
|
|
254
|
+
|
|
255
|
+
# Import Glyph processor
|
|
256
|
+
from .glyph_processor import GlyphProcessor
|
|
257
|
+
from .config import GlyphConfig
|
|
258
|
+
|
|
259
|
+
# Configure Glyph for optimal vision compression
|
|
260
|
+
config = GlyphConfig()
|
|
261
|
+
config.enabled = True
|
|
262
|
+
config.min_token_threshold = 1000
|
|
263
|
+
|
|
264
|
+
self.glyph_processor = GlyphProcessor(config=config)
|
|
265
|
+
|
|
266
|
+
def compress(
|
|
267
|
+
self,
|
|
268
|
+
text: str,
|
|
269
|
+
target_ratio: float = 30.0,
|
|
270
|
+
min_quality: float = 0.85
|
|
271
|
+
) -> Dict[str, Any]:
|
|
272
|
+
"""
|
|
273
|
+
Compress text using hybrid Glyph + Vision pipeline.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
text: Text to compress
|
|
277
|
+
target_ratio: Target compression ratio
|
|
278
|
+
min_quality: Minimum acceptable quality
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Dictionary with compression results, metrics, AND media content
|
|
282
|
+
"""
|
|
283
|
+
start_time = time.time()
|
|
284
|
+
|
|
285
|
+
# Calculate original tokens
|
|
286
|
+
from ..utils.token_utils import TokenUtils
|
|
287
|
+
original_tokens = TokenUtils.estimate_tokens(text, "gpt-4o")
|
|
288
|
+
|
|
289
|
+
self.logger.info(f"Starting hybrid compression: {original_tokens} tokens")
|
|
290
|
+
|
|
291
|
+
# Stage 1: Glyph compression
|
|
292
|
+
self.logger.info("Stage 1: Glyph rendering...")
|
|
293
|
+
glyph_start = time.time()
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
glyph_images = self.glyph_processor.process_text(
|
|
297
|
+
text,
|
|
298
|
+
provider="openai",
|
|
299
|
+
model="gpt-4o",
|
|
300
|
+
user_preference="always"
|
|
301
|
+
)
|
|
302
|
+
glyph_time = time.time() - glyph_start
|
|
303
|
+
|
|
304
|
+
# Calculate Glyph compression
|
|
305
|
+
glyph_tokens = len(glyph_images) * 1500 # Approximate
|
|
306
|
+
glyph_ratio = original_tokens / glyph_tokens if glyph_tokens > 0 else 1.0
|
|
307
|
+
|
|
308
|
+
self.logger.info(
|
|
309
|
+
f"Glyph complete: {len(glyph_images)} images, "
|
|
310
|
+
f"{glyph_ratio:.1f}x compression, {glyph_time:.2f}s"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.logger.error(f"Glyph compression failed: {e}")
|
|
315
|
+
raise
|
|
316
|
+
|
|
317
|
+
# Stage 2: Vision compression
|
|
318
|
+
self.logger.info("Stage 2: Vision compression...")
|
|
319
|
+
vision_start = time.time()
|
|
320
|
+
|
|
321
|
+
vision_result = self.vision_compressor.adaptive_compress(
|
|
322
|
+
glyph_images,
|
|
323
|
+
original_tokens,
|
|
324
|
+
target_ratio=target_ratio,
|
|
325
|
+
min_quality=min_quality
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
vision_time = time.time() - vision_start
|
|
329
|
+
|
|
330
|
+
# Calculate total compression
|
|
331
|
+
total_time = time.time() - start_time
|
|
332
|
+
total_ratio = vision_result.compression_ratio # Already calculated from original
|
|
333
|
+
|
|
334
|
+
# Compile results - NOW INCLUDING THE ACTUAL MEDIA
|
|
335
|
+
results = {
|
|
336
|
+
"success": True,
|
|
337
|
+
"media": glyph_images, # ADD THE ACTUAL COMPRESSED IMAGES HERE
|
|
338
|
+
"original_tokens": original_tokens,
|
|
339
|
+
"final_tokens": vision_result.compressed_tokens,
|
|
340
|
+
"total_compression_ratio": total_ratio,
|
|
341
|
+
"total_quality_score": vision_result.quality_score,
|
|
342
|
+
"total_processing_time": total_time,
|
|
343
|
+
"stages": {
|
|
344
|
+
"glyph": {
|
|
345
|
+
"images": len(glyph_images),
|
|
346
|
+
"tokens": glyph_tokens,
|
|
347
|
+
"ratio": glyph_ratio,
|
|
348
|
+
"time": glyph_time
|
|
349
|
+
},
|
|
350
|
+
"vision": {
|
|
351
|
+
"tokens": vision_result.compressed_tokens,
|
|
352
|
+
"ratio": vision_result.compression_ratio,
|
|
353
|
+
"quality": vision_result.quality_score,
|
|
354
|
+
"mode": vision_result.metadata["mode"],
|
|
355
|
+
"time": vision_time
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
"metadata": {
|
|
359
|
+
"pipeline": "hybrid_glyph_vision",
|
|
360
|
+
"vision_provider": self.vision_compressor.provider,
|
|
361
|
+
"vision_model": self.vision_compressor.model,
|
|
362
|
+
"timestamp": time.time()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
self.logger.info(
|
|
367
|
+
f"Hybrid compression complete: {total_ratio:.1f}x total compression, "
|
|
368
|
+
f"{vision_result.quality_score:.2%} quality, {total_time:.2f}s"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return results
|
abstractcore/config/main.py
CHANGED
|
@@ -262,6 +262,13 @@ def add_arguments(parser: argparse.ArgumentParser):
|
|
|
262
262
|
streaming_group.add_argument("--disable-streaming", action="store_true",
|
|
263
263
|
help="Disable streaming by default for CLI")
|
|
264
264
|
|
|
265
|
+
# Timeout configuration group
|
|
266
|
+
timeout_group = parser.add_argument_group('Timeout Configuration')
|
|
267
|
+
timeout_group.add_argument("--set-default-timeout", type=float, metavar="SECONDS",
|
|
268
|
+
help="Set default HTTP request timeout in seconds (default: 600 = 10 minutes)")
|
|
269
|
+
timeout_group.add_argument("--set-tool-timeout", type=float, metavar="SECONDS",
|
|
270
|
+
help="Set tool execution timeout in seconds (default: 600 = 10 minutes)")
|
|
271
|
+
|
|
265
272
|
def print_status():
|
|
266
273
|
"""Print comprehensive configuration status with improved readability."""
|
|
267
274
|
if not CONFIG_AVAILABLE or get_config_manager is None:
|
|
@@ -408,6 +415,30 @@ def print_status():
|
|
|
408
415
|
cache = status["cache"]
|
|
409
416
|
print(f"│ ✅ Configured Cache: {cache['default_cache_dir']}")
|
|
410
417
|
|
|
418
|
+
# Timeouts
|
|
419
|
+
print("│")
|
|
420
|
+
print("│ ⏱️ Timeouts")
|
|
421
|
+
timeouts = status["timeouts"]
|
|
422
|
+
default_timeout = timeouts['default_timeout']
|
|
423
|
+
tool_timeout = timeouts['tool_timeout']
|
|
424
|
+
|
|
425
|
+
# Format timeout values for display (convert seconds to minutes if >= 60)
|
|
426
|
+
def format_timeout(seconds):
|
|
427
|
+
if seconds >= 60:
|
|
428
|
+
minutes = seconds / 60
|
|
429
|
+
if minutes == int(minutes):
|
|
430
|
+
return f"{int(minutes)}m"
|
|
431
|
+
else:
|
|
432
|
+
return f"{minutes:.1f}m"
|
|
433
|
+
else:
|
|
434
|
+
return f"{int(seconds)}s"
|
|
435
|
+
|
|
436
|
+
default_timeout_str = format_timeout(default_timeout)
|
|
437
|
+
tool_timeout_str = format_timeout(tool_timeout)
|
|
438
|
+
|
|
439
|
+
print(f"│ ⏱️ HTTP Requests {default_timeout_str} ({default_timeout}s)")
|
|
440
|
+
print(f"│ 🔧 Tool Execution {tool_timeout_str} ({tool_timeout}s)")
|
|
441
|
+
|
|
411
442
|
print("└─")
|
|
412
443
|
|
|
413
444
|
# HELP SECTION - Separate actionable commands
|
|
@@ -431,6 +462,10 @@ def print_status():
|
|
|
431
462
|
print("│ abstractcore --enable-file-logging / --disable-file-logging")
|
|
432
463
|
print("│ abstractcore --set-default-cache-dir PATH")
|
|
433
464
|
print("│")
|
|
465
|
+
print("│ ⏱️ Performance & Timeouts")
|
|
466
|
+
print("│ abstractcore --set-default-timeout SECONDS (HTTP requests, default: 600)")
|
|
467
|
+
print("│ abstractcore --set-tool-timeout SECONDS (Tool execution, default: 600)")
|
|
468
|
+
print("│")
|
|
434
469
|
print("│ 🎯 Specialized Models")
|
|
435
470
|
print("│ abstractcore --set-chat-model PROVIDER/MODEL")
|
|
436
471
|
print("│ abstractcore --set-code-model PROVIDER/MODEL")
|
|
@@ -691,6 +726,35 @@ def handle_commands(args) -> bool:
|
|
|
691
726
|
print("✅ Disabled CLI streaming by default")
|
|
692
727
|
handled = True
|
|
693
728
|
|
|
729
|
+
# Timeout configuration
|
|
730
|
+
if args.set_default_timeout:
|
|
731
|
+
try:
|
|
732
|
+
config_manager.set_default_timeout(args.set_default_timeout)
|
|
733
|
+
# Format display (show in minutes if >= 60 seconds)
|
|
734
|
+
if args.set_default_timeout >= 60:
|
|
735
|
+
minutes = args.set_default_timeout / 60
|
|
736
|
+
display = f"{minutes:.1f} minutes" if minutes != int(minutes) else f"{int(minutes)} minutes"
|
|
737
|
+
else:
|
|
738
|
+
display = f"{args.set_default_timeout} seconds"
|
|
739
|
+
print(f"✅ Set default HTTP timeout to: {display} ({args.set_default_timeout}s)")
|
|
740
|
+
except ValueError as e:
|
|
741
|
+
print(f"❌ Error: {e}")
|
|
742
|
+
handled = True
|
|
743
|
+
|
|
744
|
+
if args.set_tool_timeout:
|
|
745
|
+
try:
|
|
746
|
+
config_manager.set_tool_timeout(args.set_tool_timeout)
|
|
747
|
+
# Format display (show in minutes if >= 60 seconds)
|
|
748
|
+
if args.set_tool_timeout >= 60:
|
|
749
|
+
minutes = args.set_tool_timeout / 60
|
|
750
|
+
display = f"{minutes:.1f} minutes" if minutes != int(minutes) else f"{int(minutes)} minutes"
|
|
751
|
+
else:
|
|
752
|
+
display = f"{args.set_tool_timeout} seconds"
|
|
753
|
+
print(f"✅ Set tool execution timeout to: {display} ({args.set_tool_timeout}s)")
|
|
754
|
+
except ValueError as e:
|
|
755
|
+
print(f"❌ Error: {e}")
|
|
756
|
+
handled = True
|
|
757
|
+
|
|
694
758
|
return handled
|
|
695
759
|
|
|
696
760
|
def main(argv: List[str] = None):
|
abstractcore/config/manager.py
CHANGED
|
@@ -70,6 +70,7 @@ class CacheConfig:
|
|
|
70
70
|
default_cache_dir: str = "~/.cache/abstractcore"
|
|
71
71
|
huggingface_cache_dir: str = "~/.cache/huggingface"
|
|
72
72
|
local_models_cache_dir: str = "~/.abstractcore/models"
|
|
73
|
+
glyph_cache_dir: str = "~/.abstractcore/glyph_cache"
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
@dataclass
|
|
@@ -84,6 +85,21 @@ class LoggingConfig:
|
|
|
84
85
|
file_json: bool = True
|
|
85
86
|
|
|
86
87
|
|
|
88
|
+
@dataclass
|
|
89
|
+
class TimeoutConfig:
|
|
90
|
+
"""Timeout configuration settings."""
|
|
91
|
+
default_timeout: float = 600.0 # 10 minutes for HTTP requests (in seconds)
|
|
92
|
+
tool_timeout: float = 600.0 # 10 minutes for tool execution (in seconds)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class OfflineConfig:
|
|
97
|
+
"""Offline-first configuration settings."""
|
|
98
|
+
offline_first: bool = True # AbstractCore is designed offline-first for open source LLMs
|
|
99
|
+
allow_network: bool = False # Allow network access when offline_first is True (for API providers)
|
|
100
|
+
force_local_files_only: bool = True # Force local_files_only for HuggingFace transformers
|
|
101
|
+
|
|
102
|
+
|
|
87
103
|
@dataclass
|
|
88
104
|
class AbstractCoreConfig:
|
|
89
105
|
"""Main configuration class."""
|
|
@@ -94,6 +110,8 @@ class AbstractCoreConfig:
|
|
|
94
110
|
api_keys: ApiKeysConfig
|
|
95
111
|
cache: CacheConfig
|
|
96
112
|
logging: LoggingConfig
|
|
113
|
+
timeouts: TimeoutConfig
|
|
114
|
+
offline: OfflineConfig
|
|
97
115
|
|
|
98
116
|
@classmethod
|
|
99
117
|
def default(cls):
|
|
@@ -105,7 +123,9 @@ class AbstractCoreConfig:
|
|
|
105
123
|
default_models=DefaultModels(),
|
|
106
124
|
api_keys=ApiKeysConfig(),
|
|
107
125
|
cache=CacheConfig(),
|
|
108
|
-
logging=LoggingConfig()
|
|
126
|
+
logging=LoggingConfig(),
|
|
127
|
+
timeouts=TimeoutConfig(),
|
|
128
|
+
offline=OfflineConfig()
|
|
109
129
|
)
|
|
110
130
|
|
|
111
131
|
|
|
@@ -140,6 +160,8 @@ class ConfigurationManager:
|
|
|
140
160
|
api_keys = ApiKeysConfig(**data.get('api_keys', {}))
|
|
141
161
|
cache = CacheConfig(**data.get('cache', {}))
|
|
142
162
|
logging = LoggingConfig(**data.get('logging', {}))
|
|
163
|
+
timeouts = TimeoutConfig(**data.get('timeouts', {}))
|
|
164
|
+
offline = OfflineConfig(**data.get('offline', {}))
|
|
143
165
|
|
|
144
166
|
return AbstractCoreConfig(
|
|
145
167
|
vision=vision,
|
|
@@ -148,13 +170,15 @@ class ConfigurationManager:
|
|
|
148
170
|
default_models=default_models,
|
|
149
171
|
api_keys=api_keys,
|
|
150
172
|
cache=cache,
|
|
151
|
-
logging=logging
|
|
173
|
+
logging=logging,
|
|
174
|
+
timeouts=timeouts,
|
|
175
|
+
offline=offline
|
|
152
176
|
)
|
|
153
177
|
|
|
154
178
|
def _save_config(self):
|
|
155
179
|
"""Save configuration to file."""
|
|
156
180
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
157
|
-
|
|
181
|
+
|
|
158
182
|
# Convert config to dictionary
|
|
159
183
|
config_dict = {
|
|
160
184
|
'vision': asdict(self.config.vision),
|
|
@@ -163,7 +187,9 @@ class ConfigurationManager:
|
|
|
163
187
|
'default_models': asdict(self.config.default_models),
|
|
164
188
|
'api_keys': asdict(self.config.api_keys),
|
|
165
189
|
'cache': asdict(self.config.cache),
|
|
166
|
-
'logging': asdict(self.config.logging)
|
|
190
|
+
'logging': asdict(self.config.logging),
|
|
191
|
+
'timeouts': asdict(self.config.timeouts),
|
|
192
|
+
'offline': asdict(self.config.offline)
|
|
167
193
|
}
|
|
168
194
|
|
|
169
195
|
with open(self.config_file, 'w') as f:
|
|
@@ -254,6 +280,10 @@ class ConfigurationManager:
|
|
|
254
280
|
"file_level": self.config.logging.file_level,
|
|
255
281
|
"file_logging_enabled": self.config.logging.file_logging_enabled
|
|
256
282
|
},
|
|
283
|
+
"timeouts": {
|
|
284
|
+
"default_timeout": self.config.timeouts.default_timeout,
|
|
285
|
+
"tool_timeout": self.config.timeouts.tool_timeout
|
|
286
|
+
},
|
|
257
287
|
"cache": {
|
|
258
288
|
"default_cache_dir": self.config.cache.default_cache_dir
|
|
259
289
|
},
|
|
@@ -261,6 +291,11 @@ class ConfigurationManager:
|
|
|
261
291
|
"openai": "✅ Set" if self.config.api_keys.openai else "❌ Not set",
|
|
262
292
|
"anthropic": "✅ Set" if self.config.api_keys.anthropic else "❌ Not set",
|
|
263
293
|
"google": "✅ Set" if self.config.api_keys.google else "❌ Not set"
|
|
294
|
+
},
|
|
295
|
+
"offline": {
|
|
296
|
+
"offline_first": self.config.offline.offline_first,
|
|
297
|
+
"allow_network": self.config.offline.allow_network,
|
|
298
|
+
"status": "🔒 Offline-first" if self.config.offline.offline_first else "🌐 Network-enabled"
|
|
264
299
|
}
|
|
265
300
|
}
|
|
266
301
|
|
|
@@ -327,7 +362,7 @@ class ConfigurationManager:
|
|
|
327
362
|
def get_app_default(self, app_name: str) -> Tuple[str, str]:
|
|
328
363
|
"""Get default provider and model for an app."""
|
|
329
364
|
app_defaults = self.config.app_defaults
|
|
330
|
-
|
|
365
|
+
|
|
331
366
|
if app_name == "cli":
|
|
332
367
|
return app_defaults.cli_provider, app_defaults.cli_model
|
|
333
368
|
elif app_name == "summarizer":
|
|
@@ -342,6 +377,66 @@ class ConfigurationManager:
|
|
|
342
377
|
# Return default fallback
|
|
343
378
|
return "huggingface", "unsloth/Qwen3-4B-Instruct-2507-GGUF"
|
|
344
379
|
|
|
380
|
+
def set_default_timeout(self, timeout: float) -> bool:
|
|
381
|
+
"""Set default HTTP request timeout in seconds."""
|
|
382
|
+
try:
|
|
383
|
+
if timeout <= 0:
|
|
384
|
+
raise ValueError("Timeout must be positive")
|
|
385
|
+
self.config.timeouts.default_timeout = timeout
|
|
386
|
+
self._save_config()
|
|
387
|
+
return True
|
|
388
|
+
except Exception:
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
def set_tool_timeout(self, timeout: float) -> bool:
|
|
392
|
+
"""Set tool execution timeout in seconds."""
|
|
393
|
+
try:
|
|
394
|
+
if timeout <= 0:
|
|
395
|
+
raise ValueError("Timeout must be positive")
|
|
396
|
+
self.config.timeouts.tool_timeout = timeout
|
|
397
|
+
self._save_config()
|
|
398
|
+
return True
|
|
399
|
+
except Exception:
|
|
400
|
+
return False
|
|
401
|
+
|
|
402
|
+
def get_default_timeout(self) -> float:
|
|
403
|
+
"""Get default HTTP request timeout in seconds."""
|
|
404
|
+
return self.config.timeouts.default_timeout
|
|
405
|
+
|
|
406
|
+
def get_tool_timeout(self) -> float:
|
|
407
|
+
"""Get tool execution timeout in seconds."""
|
|
408
|
+
return self.config.timeouts.tool_timeout
|
|
409
|
+
|
|
410
|
+
def set_offline_first(self, enabled: bool) -> bool:
|
|
411
|
+
"""Enable or disable offline-first mode."""
|
|
412
|
+
try:
|
|
413
|
+
self.config.offline.offline_first = enabled
|
|
414
|
+
self._save_config()
|
|
415
|
+
return True
|
|
416
|
+
except Exception:
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
def set_allow_network(self, enabled: bool) -> bool:
|
|
420
|
+
"""Allow network access when in offline-first mode."""
|
|
421
|
+
try:
|
|
422
|
+
self.config.offline.allow_network = enabled
|
|
423
|
+
self._save_config()
|
|
424
|
+
return True
|
|
425
|
+
except Exception:
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
def is_offline_first(self) -> bool:
|
|
429
|
+
"""Check if offline-first mode is enabled."""
|
|
430
|
+
return self.config.offline.offline_first
|
|
431
|
+
|
|
432
|
+
def is_network_allowed(self) -> bool:
|
|
433
|
+
"""Check if network access is allowed."""
|
|
434
|
+
return self.config.offline.allow_network
|
|
435
|
+
|
|
436
|
+
def should_force_local_files_only(self) -> bool:
|
|
437
|
+
"""Check if local_files_only should be forced for transformers."""
|
|
438
|
+
return self.config.offline.force_local_files_only
|
|
439
|
+
|
|
345
440
|
|
|
346
441
|
# Global instance
|
|
347
442
|
_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):
|