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.
Files changed (60) hide show
  1. abstractcore/__init__.py +12 -0
  2. abstractcore/apps/__main__.py +8 -1
  3. abstractcore/apps/deepsearch.py +644 -0
  4. abstractcore/apps/intent.py +614 -0
  5. abstractcore/architectures/detection.py +250 -4
  6. abstractcore/assets/architecture_formats.json +14 -1
  7. abstractcore/assets/model_capabilities.json +583 -44
  8. abstractcore/compression/__init__.py +29 -0
  9. abstractcore/compression/analytics.py +420 -0
  10. abstractcore/compression/cache.py +250 -0
  11. abstractcore/compression/config.py +279 -0
  12. abstractcore/compression/exceptions.py +30 -0
  13. abstractcore/compression/glyph_processor.py +381 -0
  14. abstractcore/compression/optimizer.py +388 -0
  15. abstractcore/compression/orchestrator.py +380 -0
  16. abstractcore/compression/pil_text_renderer.py +818 -0
  17. abstractcore/compression/quality.py +226 -0
  18. abstractcore/compression/text_formatter.py +666 -0
  19. abstractcore/compression/vision_compressor.py +371 -0
  20. abstractcore/config/main.py +66 -1
  21. abstractcore/config/manager.py +111 -5
  22. abstractcore/core/session.py +105 -5
  23. abstractcore/events/__init__.py +1 -1
  24. abstractcore/media/auto_handler.py +312 -18
  25. abstractcore/media/handlers/local_handler.py +14 -2
  26. abstractcore/media/handlers/openai_handler.py +62 -3
  27. abstractcore/media/processors/__init__.py +11 -1
  28. abstractcore/media/processors/direct_pdf_processor.py +210 -0
  29. abstractcore/media/processors/glyph_pdf_processor.py +227 -0
  30. abstractcore/media/processors/image_processor.py +7 -1
  31. abstractcore/media/processors/text_processor.py +18 -3
  32. abstractcore/media/types.py +164 -7
  33. abstractcore/processing/__init__.py +5 -1
  34. abstractcore/processing/basic_deepsearch.py +2173 -0
  35. abstractcore/processing/basic_intent.py +690 -0
  36. abstractcore/providers/__init__.py +18 -0
  37. abstractcore/providers/anthropic_provider.py +29 -2
  38. abstractcore/providers/base.py +279 -6
  39. abstractcore/providers/huggingface_provider.py +658 -27
  40. abstractcore/providers/lmstudio_provider.py +52 -2
  41. abstractcore/providers/mlx_provider.py +103 -4
  42. abstractcore/providers/model_capabilities.py +352 -0
  43. abstractcore/providers/ollama_provider.py +44 -6
  44. abstractcore/providers/openai_provider.py +29 -2
  45. abstractcore/providers/registry.py +91 -19
  46. abstractcore/server/app.py +91 -81
  47. abstractcore/structured/handler.py +161 -1
  48. abstractcore/tools/common_tools.py +98 -3
  49. abstractcore/utils/__init__.py +4 -1
  50. abstractcore/utils/cli.py +114 -1
  51. abstractcore/utils/trace_export.py +287 -0
  52. abstractcore/utils/version.py +1 -1
  53. abstractcore/utils/vlm_token_calculator.py +655 -0
  54. {abstractcore-2.5.0.dist-info → abstractcore-2.5.3.dist-info}/METADATA +140 -23
  55. abstractcore-2.5.3.dist-info/RECORD +107 -0
  56. {abstractcore-2.5.0.dist-info → abstractcore-2.5.3.dist-info}/entry_points.txt +4 -0
  57. abstractcore-2.5.0.dist-info/RECORD +0 -86
  58. {abstractcore-2.5.0.dist-info → abstractcore-2.5.3.dist-info}/WHEEL +0 -0
  59. {abstractcore-2.5.0.dist-info → abstractcore-2.5.3.dist-info}/licenses/LICENSE +0 -0
  60. {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
@@ -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):
@@ -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