abstractcore 2.5.0__py3-none-any.whl → 2.5.3__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 +12 -0
- abstractcore/apps/__main__.py +8 -1
- abstractcore/apps/deepsearch.py +644 -0
- abstractcore/apps/intent.py +614 -0
- abstractcore/architectures/detection.py +250 -4
- abstractcore/assets/architecture_formats.json +14 -1
- abstractcore/assets/model_capabilities.json +583 -44
- 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 +66 -1
- abstractcore/config/manager.py +111 -5
- abstractcore/core/session.py +105 -5
- abstractcore/events/__init__.py +1 -1
- 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/text_processor.py +18 -3
- abstractcore/media/types.py +164 -7
- abstractcore/processing/__init__.py +5 -1
- abstractcore/processing/basic_deepsearch.py +2173 -0
- abstractcore/processing/basic_intent.py +690 -0
- abstractcore/providers/__init__.py +18 -0
- abstractcore/providers/anthropic_provider.py +29 -2
- abstractcore/providers/base.py +279 -6
- abstractcore/providers/huggingface_provider.py +658 -27
- abstractcore/providers/lmstudio_provider.py +52 -2
- abstractcore/providers/mlx_provider.py +103 -4
- abstractcore/providers/model_capabilities.py +352 -0
- abstractcore/providers/ollama_provider.py +44 -6
- abstractcore/providers/openai_provider.py +29 -2
- abstractcore/providers/registry.py +91 -19
- abstractcore/server/app.py +91 -81
- abstractcore/structured/handler.py +161 -1
- abstractcore/tools/common_tools.py +98 -3
- abstractcore/utils/__init__.py +4 -1
- abstractcore/utils/cli.py +114 -1
- abstractcore/utils/trace_export.py +287 -0
- abstractcore/utils/version.py +1 -1
- abstractcore/utils/vlm_token_calculator.py +655 -0
- {abstractcore-2.5.0.dist-info → abstractcore-2.5.3.dist-info}/METADATA +140 -23
- abstractcore-2.5.3.dist-info/RECORD +107 -0
- {abstractcore-2.5.0.dist-info → abstractcore-2.5.3.dist-info}/entry_points.txt +4 -0
- abstractcore-2.5.0.dist-info/RECORD +0 -86
- {abstractcore-2.5.0.dist-info → abstractcore-2.5.3.dist-info}/WHEEL +0 -0
- {abstractcore-2.5.0.dist-info → abstractcore-2.5.3.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.5.0.dist-info → abstractcore-2.5.3.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:
|
|
@@ -289,7 +296,8 @@ def print_status():
|
|
|
289
296
|
("CLI (utils)", app_defaults["cli"]),
|
|
290
297
|
("Summarizer", app_defaults["summarizer"]),
|
|
291
298
|
("Extractor", app_defaults["extractor"]),
|
|
292
|
-
("Judge", app_defaults["judge"])
|
|
299
|
+
("Judge", app_defaults["judge"]),
|
|
300
|
+
("Intent", app_defaults["intent"])
|
|
293
301
|
]
|
|
294
302
|
|
|
295
303
|
for app_name, app_info in apps:
|
|
@@ -407,6 +415,30 @@ def print_status():
|
|
|
407
415
|
cache = status["cache"]
|
|
408
416
|
print(f"│ ✅ Configured Cache: {cache['default_cache_dir']}")
|
|
409
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
|
+
|
|
410
442
|
print("└─")
|
|
411
443
|
|
|
412
444
|
# HELP SECTION - Separate actionable commands
|
|
@@ -430,6 +462,10 @@ def print_status():
|
|
|
430
462
|
print("│ abstractcore --enable-file-logging / --disable-file-logging")
|
|
431
463
|
print("│ abstractcore --set-default-cache-dir PATH")
|
|
432
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("│")
|
|
433
469
|
print("│ 🎯 Specialized Models")
|
|
434
470
|
print("│ abstractcore --set-chat-model PROVIDER/MODEL")
|
|
435
471
|
print("│ abstractcore --set-code-model PROVIDER/MODEL")
|
|
@@ -690,6 +726,35 @@ def handle_commands(args) -> bool:
|
|
|
690
726
|
print("✅ Disabled CLI streaming by default")
|
|
691
727
|
handled = True
|
|
692
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
|
+
|
|
693
758
|
return handled
|
|
694
759
|
|
|
695
760
|
def main(argv: List[str] = None):
|
abstractcore/config/manager.py
CHANGED
|
@@ -43,6 +43,8 @@ class AppDefaults:
|
|
|
43
43
|
extractor_model: Optional[str] = "unsloth/Qwen3-4B-Instruct-2507-GGUF"
|
|
44
44
|
judge_provider: Optional[str] = "huggingface"
|
|
45
45
|
judge_model: Optional[str] = "unsloth/Qwen3-4B-Instruct-2507-GGUF"
|
|
46
|
+
intent_provider: Optional[str] = "huggingface"
|
|
47
|
+
intent_model: Optional[str] = "unsloth/Qwen3-4B-Instruct-2507-GGUF"
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
@dataclass
|
|
@@ -68,6 +70,7 @@ class CacheConfig:
|
|
|
68
70
|
default_cache_dir: str = "~/.cache/abstractcore"
|
|
69
71
|
huggingface_cache_dir: str = "~/.cache/huggingface"
|
|
70
72
|
local_models_cache_dir: str = "~/.abstractcore/models"
|
|
73
|
+
glyph_cache_dir: str = "~/.abstractcore/glyph_cache"
|
|
71
74
|
|
|
72
75
|
|
|
73
76
|
@dataclass
|
|
@@ -82,6 +85,21 @@ class LoggingConfig:
|
|
|
82
85
|
file_json: bool = True
|
|
83
86
|
|
|
84
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
|
+
|
|
85
103
|
@dataclass
|
|
86
104
|
class AbstractCoreConfig:
|
|
87
105
|
"""Main configuration class."""
|
|
@@ -92,6 +110,8 @@ class AbstractCoreConfig:
|
|
|
92
110
|
api_keys: ApiKeysConfig
|
|
93
111
|
cache: CacheConfig
|
|
94
112
|
logging: LoggingConfig
|
|
113
|
+
timeouts: TimeoutConfig
|
|
114
|
+
offline: OfflineConfig
|
|
95
115
|
|
|
96
116
|
@classmethod
|
|
97
117
|
def default(cls):
|
|
@@ -103,7 +123,9 @@ class AbstractCoreConfig:
|
|
|
103
123
|
default_models=DefaultModels(),
|
|
104
124
|
api_keys=ApiKeysConfig(),
|
|
105
125
|
cache=CacheConfig(),
|
|
106
|
-
logging=LoggingConfig()
|
|
126
|
+
logging=LoggingConfig(),
|
|
127
|
+
timeouts=TimeoutConfig(),
|
|
128
|
+
offline=OfflineConfig()
|
|
107
129
|
)
|
|
108
130
|
|
|
109
131
|
|
|
@@ -138,6 +160,8 @@ class ConfigurationManager:
|
|
|
138
160
|
api_keys = ApiKeysConfig(**data.get('api_keys', {}))
|
|
139
161
|
cache = CacheConfig(**data.get('cache', {}))
|
|
140
162
|
logging = LoggingConfig(**data.get('logging', {}))
|
|
163
|
+
timeouts = TimeoutConfig(**data.get('timeouts', {}))
|
|
164
|
+
offline = OfflineConfig(**data.get('offline', {}))
|
|
141
165
|
|
|
142
166
|
return AbstractCoreConfig(
|
|
143
167
|
vision=vision,
|
|
@@ -146,13 +170,15 @@ class ConfigurationManager:
|
|
|
146
170
|
default_models=default_models,
|
|
147
171
|
api_keys=api_keys,
|
|
148
172
|
cache=cache,
|
|
149
|
-
logging=logging
|
|
173
|
+
logging=logging,
|
|
174
|
+
timeouts=timeouts,
|
|
175
|
+
offline=offline
|
|
150
176
|
)
|
|
151
177
|
|
|
152
178
|
def _save_config(self):
|
|
153
179
|
"""Save configuration to file."""
|
|
154
180
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
155
|
-
|
|
181
|
+
|
|
156
182
|
# Convert config to dictionary
|
|
157
183
|
config_dict = {
|
|
158
184
|
'vision': asdict(self.config.vision),
|
|
@@ -161,7 +187,9 @@ class ConfigurationManager:
|
|
|
161
187
|
'default_models': asdict(self.config.default_models),
|
|
162
188
|
'api_keys': asdict(self.config.api_keys),
|
|
163
189
|
'cache': asdict(self.config.cache),
|
|
164
|
-
'logging': asdict(self.config.logging)
|
|
190
|
+
'logging': asdict(self.config.logging),
|
|
191
|
+
'timeouts': asdict(self.config.timeouts),
|
|
192
|
+
'offline': asdict(self.config.offline)
|
|
165
193
|
}
|
|
166
194
|
|
|
167
195
|
with open(self.config_file, 'w') as f:
|
|
@@ -227,6 +255,10 @@ class ConfigurationManager:
|
|
|
227
255
|
"judge": {
|
|
228
256
|
"provider": self.config.app_defaults.judge_provider,
|
|
229
257
|
"model": self.config.app_defaults.judge_model
|
|
258
|
+
},
|
|
259
|
+
"intent": {
|
|
260
|
+
"provider": self.config.app_defaults.intent_provider,
|
|
261
|
+
"model": self.config.app_defaults.intent_model
|
|
230
262
|
}
|
|
231
263
|
},
|
|
232
264
|
"global_defaults": {
|
|
@@ -248,6 +280,10 @@ class ConfigurationManager:
|
|
|
248
280
|
"file_level": self.config.logging.file_level,
|
|
249
281
|
"file_logging_enabled": self.config.logging.file_logging_enabled
|
|
250
282
|
},
|
|
283
|
+
"timeouts": {
|
|
284
|
+
"default_timeout": self.config.timeouts.default_timeout,
|
|
285
|
+
"tool_timeout": self.config.timeouts.tool_timeout
|
|
286
|
+
},
|
|
251
287
|
"cache": {
|
|
252
288
|
"default_cache_dir": self.config.cache.default_cache_dir
|
|
253
289
|
},
|
|
@@ -255,6 +291,11 @@ class ConfigurationManager:
|
|
|
255
291
|
"openai": "✅ Set" if self.config.api_keys.openai else "❌ Not set",
|
|
256
292
|
"anthropic": "✅ Set" if self.config.api_keys.anthropic else "❌ Not set",
|
|
257
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"
|
|
258
299
|
}
|
|
259
300
|
}
|
|
260
301
|
|
|
@@ -290,6 +331,9 @@ class ConfigurationManager:
|
|
|
290
331
|
elif app_name == "judge":
|
|
291
332
|
self.config.app_defaults.judge_provider = provider
|
|
292
333
|
self.config.app_defaults.judge_model = model
|
|
334
|
+
elif app_name == "intent":
|
|
335
|
+
self.config.app_defaults.intent_provider = provider
|
|
336
|
+
self.config.app_defaults.intent_model = model
|
|
293
337
|
else:
|
|
294
338
|
raise ValueError(f"Unknown app: {app_name}")
|
|
295
339
|
|
|
@@ -318,7 +362,7 @@ class ConfigurationManager:
|
|
|
318
362
|
def get_app_default(self, app_name: str) -> Tuple[str, str]:
|
|
319
363
|
"""Get default provider and model for an app."""
|
|
320
364
|
app_defaults = self.config.app_defaults
|
|
321
|
-
|
|
365
|
+
|
|
322
366
|
if app_name == "cli":
|
|
323
367
|
return app_defaults.cli_provider, app_defaults.cli_model
|
|
324
368
|
elif app_name == "summarizer":
|
|
@@ -327,10 +371,72 @@ class ConfigurationManager:
|
|
|
327
371
|
return app_defaults.extractor_provider, app_defaults.extractor_model
|
|
328
372
|
elif app_name == "judge":
|
|
329
373
|
return app_defaults.judge_provider, app_defaults.judge_model
|
|
374
|
+
elif app_name == "intent":
|
|
375
|
+
return app_defaults.intent_provider, app_defaults.intent_model
|
|
330
376
|
else:
|
|
331
377
|
# Return default fallback
|
|
332
378
|
return "huggingface", "unsloth/Qwen3-4B-Instruct-2507-GGUF"
|
|
333
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
|
+
|
|
334
440
|
|
|
335
441
|
# Global instance
|
|
336
442
|
_config_manager = None
|