DeepFabric 4.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. deepfabric/__init__.py +70 -0
  2. deepfabric/__main__.py +6 -0
  3. deepfabric/auth.py +382 -0
  4. deepfabric/builders.py +303 -0
  5. deepfabric/builders_agent.py +1304 -0
  6. deepfabric/cli.py +1288 -0
  7. deepfabric/config.py +899 -0
  8. deepfabric/config_manager.py +251 -0
  9. deepfabric/constants.py +94 -0
  10. deepfabric/dataset_manager.py +534 -0
  11. deepfabric/error_codes.py +581 -0
  12. deepfabric/evaluation/__init__.py +47 -0
  13. deepfabric/evaluation/backends/__init__.py +32 -0
  14. deepfabric/evaluation/backends/ollama_backend.py +137 -0
  15. deepfabric/evaluation/backends/tool_call_parsers.py +409 -0
  16. deepfabric/evaluation/backends/transformers_backend.py +326 -0
  17. deepfabric/evaluation/evaluator.py +845 -0
  18. deepfabric/evaluation/evaluators/__init__.py +13 -0
  19. deepfabric/evaluation/evaluators/base.py +104 -0
  20. deepfabric/evaluation/evaluators/builtin/__init__.py +5 -0
  21. deepfabric/evaluation/evaluators/builtin/tool_calling.py +93 -0
  22. deepfabric/evaluation/evaluators/registry.py +66 -0
  23. deepfabric/evaluation/inference.py +155 -0
  24. deepfabric/evaluation/metrics.py +397 -0
  25. deepfabric/evaluation/parser.py +304 -0
  26. deepfabric/evaluation/reporters/__init__.py +13 -0
  27. deepfabric/evaluation/reporters/base.py +56 -0
  28. deepfabric/evaluation/reporters/cloud_reporter.py +195 -0
  29. deepfabric/evaluation/reporters/file_reporter.py +61 -0
  30. deepfabric/evaluation/reporters/multi_reporter.py +56 -0
  31. deepfabric/exceptions.py +67 -0
  32. deepfabric/factory.py +26 -0
  33. deepfabric/generator.py +1084 -0
  34. deepfabric/graph.py +545 -0
  35. deepfabric/hf_hub.py +214 -0
  36. deepfabric/kaggle_hub.py +219 -0
  37. deepfabric/llm/__init__.py +41 -0
  38. deepfabric/llm/api_key_verifier.py +534 -0
  39. deepfabric/llm/client.py +1206 -0
  40. deepfabric/llm/errors.py +105 -0
  41. deepfabric/llm/rate_limit_config.py +262 -0
  42. deepfabric/llm/rate_limit_detector.py +278 -0
  43. deepfabric/llm/retry_handler.py +270 -0
  44. deepfabric/metrics.py +212 -0
  45. deepfabric/progress.py +262 -0
  46. deepfabric/prompts.py +290 -0
  47. deepfabric/schemas.py +1000 -0
  48. deepfabric/spin/__init__.py +6 -0
  49. deepfabric/spin/client.py +263 -0
  50. deepfabric/spin/models.py +26 -0
  51. deepfabric/stream_simulator.py +90 -0
  52. deepfabric/tools/__init__.py +5 -0
  53. deepfabric/tools/defaults.py +85 -0
  54. deepfabric/tools/loader.py +87 -0
  55. deepfabric/tools/mcp_client.py +677 -0
  56. deepfabric/topic_manager.py +303 -0
  57. deepfabric/topic_model.py +20 -0
  58. deepfabric/training/__init__.py +35 -0
  59. deepfabric/training/api_key_prompt.py +302 -0
  60. deepfabric/training/callback.py +363 -0
  61. deepfabric/training/metrics_sender.py +301 -0
  62. deepfabric/tree.py +438 -0
  63. deepfabric/tui.py +1267 -0
  64. deepfabric/update_checker.py +166 -0
  65. deepfabric/utils.py +150 -0
  66. deepfabric/validation.py +143 -0
  67. deepfabric-4.4.0.dist-info/METADATA +702 -0
  68. deepfabric-4.4.0.dist-info/RECORD +71 -0
  69. deepfabric-4.4.0.dist-info/WHEEL +4 -0
  70. deepfabric-4.4.0.dist-info/entry_points.txt +2 -0
  71. deepfabric-4.4.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,270 @@
1
+ """Retry handler with intelligent backoff for LLM API calls."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import random
6
+ import time
7
+
8
+ from collections.abc import Callable, Coroutine
9
+ from functools import wraps
10
+ from typing import Any, TypeVar
11
+
12
+ from .rate_limit_config import BackoffStrategy, RateLimitConfig
13
+ from .rate_limit_detector import RateLimitDetector
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ T = TypeVar("T")
18
+
19
+
20
+ class RetryHandler:
21
+ """Intelligent retry handler for LLM API calls with provider-aware backoff."""
22
+
23
+ def __init__(self, config: RateLimitConfig, provider: str):
24
+ """Initialize retry handler.
25
+
26
+ Args:
27
+ config: Rate limit configuration
28
+ provider: Provider name (openai, anthropic, gemini, ollama)
29
+ """
30
+ self.config = config
31
+ self.provider = provider
32
+ self.detector = RateLimitDetector()
33
+
34
+ def should_retry(self, exception: Exception) -> bool:
35
+ """Determine if an exception should trigger a retry.
36
+
37
+ Args:
38
+ exception: The exception that occurred
39
+
40
+ Returns:
41
+ True if the error is retryable
42
+ """
43
+ # Check if it's a retryable error (rate limit, timeout, server error)
44
+ if not self.detector.is_retryable_error(exception, self.provider):
45
+ return False
46
+
47
+ # If it's a rate limit error, check if we should fail fast
48
+ if self.detector.is_rate_limit_error(exception, self.provider):
49
+ quota_info = self.detector.extract_quota_info(exception, self.provider)
50
+
51
+ # Don't retry if we should fail fast (e.g., daily quota exhausted)
52
+ if self.detector.should_fail_fast(quota_info):
53
+ logger.warning(
54
+ "Failing fast for %s: %s (quota_info: %s)",
55
+ self.provider,
56
+ exception,
57
+ quota_info,
58
+ )
59
+ return False
60
+
61
+ return True
62
+
63
+ def calculate_delay(
64
+ self,
65
+ attempt: int,
66
+ exception: Exception | None = None,
67
+ ) -> float:
68
+ """Calculate the delay before the next retry attempt.
69
+
70
+ Args:
71
+ attempt: Current attempt number (0-indexed)
72
+ exception: Optional exception to extract retry-after from
73
+
74
+ Returns:
75
+ Delay in seconds
76
+ """
77
+ # Extract retry-after from exception if available
78
+ retry_after = None
79
+ if exception and self.config.respect_retry_after:
80
+ quota_info = self.detector.extract_quota_info(exception, self.provider)
81
+ retry_after = quota_info.retry_after
82
+
83
+ # If provider specifies retry-after, use it (with max_delay cap)
84
+ if retry_after is not None:
85
+ delay = min(retry_after, self.config.max_delay)
86
+ logger.debug("Using retry-after header: %.2fs (capped at %.2fs)", retry_after, delay)
87
+ return delay
88
+
89
+ # Otherwise calculate delay based on backoff strategy
90
+ if self.config.backoff_strategy == BackoffStrategy.EXPONENTIAL:
91
+ delay = self.config.base_delay * (self.config.exponential_base**attempt)
92
+ elif self.config.backoff_strategy == BackoffStrategy.EXPONENTIAL_JITTER:
93
+ base_delay = self.config.base_delay * (self.config.exponential_base**attempt)
94
+ delay = base_delay
95
+ elif self.config.backoff_strategy == BackoffStrategy.LINEAR:
96
+ delay = self.config.base_delay * (attempt + 1)
97
+ elif self.config.backoff_strategy == BackoffStrategy.CONSTANT:
98
+ delay = self.config.base_delay
99
+ else:
100
+ # Default to exponential
101
+ delay = self.config.base_delay * (self.config.exponential_base**attempt)
102
+
103
+ # Apply jitter if enabled
104
+ if self.config.jitter:
105
+ # Add random jitter of ±25% to prevent thundering herd
106
+ jitter_range = delay * 0.25
107
+ delay = delay + random.uniform(-jitter_range, jitter_range) # noqa: S311 # nosec
108
+
109
+ # Ensure delay is within bounds and return
110
+ return max(self.config.base_delay, min(delay, self.config.max_delay))
111
+
112
+ def on_backoff_handler(self, details: dict[str, Any]) -> None:
113
+ """Callback for backoff retry attempts.
114
+
115
+ Args:
116
+ details: Backoff details including attempt number, wait time, exception
117
+ """
118
+ exception = details.get("exception")
119
+ wait = details.get("wait", 0)
120
+ tries = details.get("tries", 0)
121
+
122
+ # Extract quota information if it's a rate limit error
123
+ quota_info_str = ""
124
+ if exception and self.detector.is_rate_limit_error(exception, self.provider):
125
+ quota_info = self.detector.extract_quota_info(exception, self.provider)
126
+ if quota_info.quota_type:
127
+ quota_info_str = f" (quota_type: {quota_info.quota_type})"
128
+
129
+ logger.warning(
130
+ "Rate limit/transient error for %s on attempt %d, backing off %.2fs%s: %s",
131
+ self.provider,
132
+ tries,
133
+ wait,
134
+ quota_info_str,
135
+ exception,
136
+ )
137
+
138
+ def on_giveup_handler(self, details: dict[str, Any]) -> None:
139
+ """Callback when giving up after max retries.
140
+
141
+ Args:
142
+ details: Backoff details including final exception
143
+ """
144
+ exception = details.get("exception")
145
+ tries = details.get("tries", 0)
146
+
147
+ logger.error(
148
+ "Giving up after %d attempts for %s: %s",
149
+ tries,
150
+ self.provider,
151
+ exception,
152
+ )
153
+
154
+
155
+ def retry_with_backoff(func: Callable[..., T]) -> Callable[..., T]:
156
+ """Decorator to add retry logic with backoff to synchronous functions.
157
+
158
+ This decorator is applied to LLMClient methods to handle rate limits
159
+ and transient errors automatically.
160
+
161
+ Args:
162
+ func: Function to wrap with retry logic
163
+
164
+ Returns:
165
+ Wrapped function with retry capability
166
+ """
167
+
168
+ @wraps(func)
169
+ def wrapper(self, *args: Any, **kwargs: Any) -> T:
170
+ # Get retry handler from self (assumes LLMClient instance)
171
+ retry_handler = getattr(self, "retry_handler", None)
172
+ if not retry_handler or not isinstance(retry_handler, RetryHandler):
173
+ # No retry handler configured, call function directly
174
+ return func(self, *args, **kwargs)
175
+
176
+ config = retry_handler.config
177
+ attempt = 0
178
+
179
+ while attempt <= config.max_retries:
180
+ try:
181
+ return func(self, *args, **kwargs)
182
+ except Exception as e:
183
+ # Check if we should retry
184
+ if not retry_handler.should_retry(e):
185
+ # Not retryable, raise immediately
186
+ raise
187
+
188
+ # Check if we've exhausted retries
189
+ if attempt >= config.max_retries:
190
+ retry_handler.on_giveup_handler({"exception": e, "tries": attempt + 1})
191
+ raise
192
+
193
+ # Calculate delay and wait
194
+ delay = retry_handler.calculate_delay(attempt, e)
195
+ retry_handler.on_backoff_handler(
196
+ {
197
+ "exception": e,
198
+ "wait": delay,
199
+ "tries": attempt + 1,
200
+ }
201
+ )
202
+
203
+ time.sleep(delay)
204
+ attempt += 1
205
+
206
+ # Should never reach here, but for type safety
207
+ msg = "Unexpected state in retry logic"
208
+ raise RuntimeError(msg)
209
+
210
+ return wrapper
211
+
212
+
213
+ def retry_with_backoff_async(
214
+ func: Callable[..., Coroutine[Any, Any, T]],
215
+ ) -> Callable[..., Coroutine[Any, Any, T]]:
216
+ """Decorator to add retry logic with backoff to async functions.
217
+
218
+ This decorator is applied to async LLMClient methods to handle rate limits
219
+ and transient errors automatically.
220
+
221
+ Args:
222
+ func: Async function to wrap with retry logic
223
+
224
+ Returns:
225
+ Wrapped async function with retry capability
226
+ """
227
+
228
+ @wraps(func)
229
+ async def wrapper(self, *args: Any, **kwargs: Any) -> T:
230
+ # Get retry handler from self (assumes LLMClient instance)
231
+ retry_handler = getattr(self, "retry_handler", None)
232
+ if not retry_handler or not isinstance(retry_handler, RetryHandler):
233
+ # No retry handler configured, call function directly
234
+ return await func(self, *args, **kwargs)
235
+
236
+ config = retry_handler.config
237
+ attempt = 0
238
+
239
+ while attempt <= config.max_retries:
240
+ try:
241
+ return await func(self, *args, **kwargs)
242
+ except Exception as e:
243
+ # Check if we should retry
244
+ if not retry_handler.should_retry(e):
245
+ # Not retryable, raise immediately
246
+ raise
247
+
248
+ # Check if we've exhausted retries
249
+ if attempt >= config.max_retries:
250
+ retry_handler.on_giveup_handler({"exception": e, "tries": attempt + 1})
251
+ raise
252
+
253
+ # Calculate delay and wait
254
+ delay = retry_handler.calculate_delay(attempt, e)
255
+ retry_handler.on_backoff_handler(
256
+ {
257
+ "exception": e,
258
+ "wait": delay,
259
+ "tries": attempt + 1,
260
+ }
261
+ )
262
+
263
+ await asyncio.sleep(delay)
264
+ attempt += 1
265
+
266
+ # Should never reach here, but for type safety
267
+ msg = "Unexpected state in retry logic"
268
+ raise RuntimeError(msg)
269
+
270
+ return wrapper
deepfabric/metrics.py ADDED
@@ -0,0 +1,212 @@
1
+ import contextlib
2
+ import logging
3
+ import os
4
+ import uuid
5
+
6
+ from pathlib import Path
7
+
8
+ from posthog import Posthog, identify_context, new_context
9
+
10
+ from .tui import get_tui
11
+
12
+ try:
13
+ import importlib.metadata
14
+
15
+ VERSION = importlib.metadata.version("deepfabric")
16
+ except (ImportError, importlib.metadata.PackageNotFoundError):
17
+ VERSION = "development"
18
+
19
+
20
+ # Initialize PostHog client
21
+ posthog = Posthog(
22
+ project_api_key="phc_Kn8hKQIXHm5OHp5OTxvMvFDUmT7HyOUNlJvWkduB9qO",
23
+ host="https://us.i.posthog.com",
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class _TelemetryState:
30
+ """Holds the state for the telemetry module."""
31
+
32
+ def __init__(self) -> None:
33
+ self.debug_trace: bool = False
34
+ self.user_id_announced: bool = False
35
+ self.user_id_cache: str | None = None
36
+ self.telemetry_failed_once: bool = False
37
+
38
+
39
+ _state = _TelemetryState()
40
+
41
+
42
+ APP_NAME = "DeepFabric"
43
+ APP_AUTHOR = "DeepFabric"
44
+
45
+
46
+ try:
47
+ from platformdirs import user_data_dir
48
+ except ImportError: # pragma: no cover - optional dependency
49
+
50
+ def user_data_dir(appname: str, appauthor: str | None = None) -> str:
51
+ if os.name == "nt":
52
+ base = os.environ.get("APPDATA") or os.path.expanduser(r"~\AppData\Roaming")
53
+ elif os.name == "posix":
54
+ base = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share")
55
+ else:
56
+ base = os.path.expanduser("~")
57
+ return str(Path(base) / (appauthor or appname) / appname)
58
+
59
+
60
+ def _user_id_path() -> Path:
61
+ candidates: list[Path] = []
62
+ try:
63
+ candidates.append(Path(user_data_dir(APP_NAME, APP_AUTHOR)))
64
+ except Exception:
65
+ logger.debug("Failed to resolve platform data dir", exc_info=True)
66
+ candidates.append(Path.home() / f".{APP_NAME.lower()}")
67
+ candidates.append(Path.cwd())
68
+
69
+ for data_dir in candidates:
70
+ try:
71
+ data_dir.mkdir(parents=True, exist_ok=True)
72
+ return data_dir / "telemetry_id"
73
+ except Exception:
74
+ logger.debug("Failed to prepare telemetry directory %s", data_dir, exc_info=True)
75
+
76
+ return Path("telemetry_id")
77
+
78
+
79
+ def _read_user_id(path: Path) -> str | None:
80
+ try:
81
+ if path.exists():
82
+ candidate = path.read_text(encoding="utf-8").strip()
83
+ if candidate:
84
+ uuid.UUID(candidate)
85
+ return candidate
86
+ except Exception:
87
+ logger.debug("Failed to read existing telemetry id", exc_info=True)
88
+ return None
89
+
90
+
91
+ def _write_user_id(path: Path) -> str:
92
+ user_id = str(uuid.uuid4())
93
+ tmp_path = path.with_suffix(".tmp")
94
+ try:
95
+ tmp_path.write_text(user_id, encoding="utf-8")
96
+ if os.name == "posix":
97
+ os.chmod(tmp_path, 0o600)
98
+ tmp_path.replace(path)
99
+ except Exception:
100
+ logger.debug("Failed to persist telemetry id", exc_info=True)
101
+ return user_id
102
+ else:
103
+ return user_id
104
+ finally:
105
+ with contextlib.suppress(Exception):
106
+ if tmp_path.exists() and tmp_path != path:
107
+ tmp_path.unlink()
108
+
109
+
110
+ def _get_user_id() -> str:
111
+ """Generate a stable, anonymous user ID persisted on disk."""
112
+ if _state.user_id_cache is not None:
113
+ return _state.user_id_cache
114
+
115
+ path = _user_id_path()
116
+ user_id = _read_user_id(path)
117
+ if user_id is None:
118
+ user_id = _write_user_id(path)
119
+
120
+ _state.user_id_cache = user_id
121
+ return user_id
122
+
123
+
124
+ def _is_developer() -> bool:
125
+ """
126
+ Check if this session is marked as a developer session.
127
+
128
+ Returns:
129
+ bool: True if DEEPFABRIC_DEVELOPER environment variable is set to 'True'
130
+ """
131
+ return os.environ.get("DEEPFABRIC_DEVELOPER") == "True"
132
+
133
+
134
+ def set_trace_debug(enabled: bool) -> None:
135
+ """Enable or disable debug output for telemetry events."""
136
+ _state.debug_trace = enabled
137
+ if not enabled:
138
+ _state.user_id_announced = False
139
+
140
+
141
+ def _announce_user_id(user_id: str) -> None:
142
+ if _state.user_id_announced or not _state.debug_trace:
143
+ return
144
+ try:
145
+ get_tui().info(f"metrics user id: {user_id}")
146
+ except Exception: # pragma: no cover - fallback to logging
147
+ logger.debug("metrics user id: %s", user_id)
148
+
149
+ _state.user_id_announced = True
150
+
151
+
152
+ def trace(event_name, event_properties=None):
153
+ """
154
+ Send an analytics event if metrics is enabled.
155
+
156
+ Uses privacy-respecting identity tracking with a stable, anonymous user ID
157
+ stored on disk for reuse. Developer sessions are marked with
158
+ the is_developer flag when DEEPFABRIC_DEVELOPER=True.
159
+
160
+ Args:
161
+ event_name: Name of the event to track
162
+ event_properties: Optional dictionary of event properties
163
+ """
164
+ if not is_enabled():
165
+ return
166
+
167
+ try:
168
+ # Generate stable user ID
169
+ user_id = _get_user_id()
170
+ _announce_user_id(user_id)
171
+
172
+ # Add version and developer flag to all events
173
+ properties = event_properties or {}
174
+ properties["version"] = VERSION
175
+ properties["is_developer"] = _is_developer()
176
+
177
+ # Use identity context to associate events with the user
178
+ with new_context():
179
+ identify_context(user_id)
180
+ posthog.capture(
181
+ distinct_id=user_id,
182
+ event=event_name,
183
+ properties=properties,
184
+ )
185
+ except Exception:
186
+ if not _state.telemetry_failed_once:
187
+ _state.telemetry_failed_once = True
188
+ logger.warning(
189
+ "Failed to send telemetry event. Further failures will be logged at DEBUG level.",
190
+ exc_info=True,
191
+ )
192
+ else:
193
+ logger.debug("Failed to capture metrics event", exc_info=True)
194
+
195
+
196
+ def is_enabled():
197
+ """Check if analytics is currently enabled."""
198
+ return (
199
+ os.environ.get("ANONYMIZED_TELEMETRY") != "False"
200
+ and os.environ.get("DEEPFABRIC_TESTING") != "True"
201
+ )
202
+
203
+
204
+ def shutdown() -> None:
205
+ """
206
+ Shutdown the PostHog client, flushing any buffered events.
207
+
208
+ This should be called on application exit to ensure all metrics data is sent.
209
+ """
210
+ if is_enabled():
211
+ logger.debug("Shutting down metrics client.")
212
+ posthog.shutdown()