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,534 @@
1
+ """API key verification for LLM providers.
2
+
3
+ This module provides functionality to verify that API keys are valid and working
4
+ by making lightweight test API calls to each provider.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import Any
13
+
14
+ import anthropic
15
+ import openai
16
+
17
+ from google import genai
18
+ from google.api_core import exceptions as google_exceptions
19
+
20
+ from .client import PROVIDER_API_KEY_MAP
21
+
22
+
23
+ class VerificationStatus(Enum):
24
+ """Status of an API key verification check."""
25
+
26
+ VALID = "valid"
27
+ INVALID = "invalid"
28
+ MISSING = "missing"
29
+ CONNECTION_ERROR = "connection_error"
30
+ RATE_LIMITED = "rate_limited"
31
+ UNKNOWN_ERROR = "unknown_error"
32
+ NOT_APPLICABLE = "not_applicable" # For providers like Ollama that don't need keys
33
+
34
+
35
+ @dataclass
36
+ class VerificationResult:
37
+ """Result of an API key verification check."""
38
+
39
+ provider: str
40
+ status: VerificationStatus
41
+ message: str
42
+ api_key_env_var: str | None = None
43
+ details: dict[str, Any] = field(default_factory=dict)
44
+
45
+ @property
46
+ def is_valid(self) -> bool:
47
+ """Check if the verification passed."""
48
+ return self.status in (VerificationStatus.VALID, VerificationStatus.NOT_APPLICABLE)
49
+
50
+ def __str__(self) -> str:
51
+ return f"{self.provider}: {self.status.value} - {self.message}"
52
+
53
+
54
+ def verify_openai_api_key(api_key: str | None = None) -> VerificationResult:
55
+ """Verify an OpenAI API key by making a lightweight API call.
56
+
57
+ Args:
58
+ api_key: Optional API key to verify. If not provided, uses OPENAI_API_KEY env var.
59
+
60
+ Returns:
61
+ VerificationResult with status and details
62
+ """
63
+ env_var = "OPENAI_API_KEY"
64
+
65
+ if api_key is None:
66
+ api_key = os.getenv(env_var)
67
+
68
+ if not api_key:
69
+ return VerificationResult(
70
+ provider="openai",
71
+ status=VerificationStatus.MISSING,
72
+ message=f"{env_var} environment variable is not set",
73
+ api_key_env_var=env_var,
74
+ )
75
+
76
+ try:
77
+ client = openai.OpenAI(api_key=api_key)
78
+ # Use the models list endpoint - it's lightweight and verifies auth
79
+ models = client.models.list()
80
+ # Just check we can iterate (don't need to consume all)
81
+ model_count = sum(1 for _ in models)
82
+
83
+ return VerificationResult(
84
+ provider="openai",
85
+ status=VerificationStatus.VALID,
86
+ message="API key is valid",
87
+ api_key_env_var=env_var,
88
+ details={"models_available": model_count},
89
+ )
90
+
91
+ except openai.AuthenticationError as e:
92
+ return VerificationResult(
93
+ provider="openai",
94
+ status=VerificationStatus.INVALID,
95
+ message="Invalid API key",
96
+ api_key_env_var=env_var,
97
+ details={"error": str(e)},
98
+ )
99
+
100
+ except openai.RateLimitError as e:
101
+ return VerificationResult(
102
+ provider="openai",
103
+ status=VerificationStatus.RATE_LIMITED,
104
+ message="Rate limit exceeded (key may be valid but quota exhausted)",
105
+ api_key_env_var=env_var,
106
+ details={"error": str(e)},
107
+ )
108
+
109
+ except openai.APIConnectionError as e:
110
+ return VerificationResult(
111
+ provider="openai",
112
+ status=VerificationStatus.CONNECTION_ERROR,
113
+ message="Failed to connect to OpenAI API",
114
+ api_key_env_var=env_var,
115
+ details={"error": str(e)},
116
+ )
117
+
118
+ except Exception as e:
119
+ return VerificationResult(
120
+ provider="openai",
121
+ status=VerificationStatus.UNKNOWN_ERROR,
122
+ message=f"Unexpected error: {e}",
123
+ api_key_env_var=env_var,
124
+ details={"error": str(e), "error_type": type(e).__name__},
125
+ )
126
+
127
+
128
+ def verify_anthropic_api_key(api_key: str | None = None) -> VerificationResult:
129
+ """Verify an Anthropic API key by making a lightweight API call.
130
+
131
+ Args:
132
+ api_key: Optional API key to verify. If not provided, uses ANTHROPIC_API_KEY env var.
133
+
134
+ Returns:
135
+ VerificationResult with status and details
136
+ """
137
+ env_var = "ANTHROPIC_API_KEY"
138
+
139
+ if api_key is None:
140
+ api_key = os.getenv(env_var)
141
+
142
+ if not api_key:
143
+ return VerificationResult(
144
+ provider="anthropic",
145
+ status=VerificationStatus.MISSING,
146
+ message=f"{env_var} environment variable is not set",
147
+ api_key_env_var=env_var,
148
+ )
149
+
150
+ try:
151
+ client = anthropic.Anthropic(api_key=api_key)
152
+ # Use a minimal message call - this is the simplest way to verify auth
153
+ # The API doesn't have a models list endpoint like OpenAI
154
+ # Using count_tokens is a read-only operation that verifies the key
155
+ _result = client.messages.count_tokens(
156
+ model="claude-3-haiku-20240307",
157
+ messages=[{"role": "user", "content": "test"}],
158
+ )
159
+
160
+ return VerificationResult(
161
+ provider="anthropic",
162
+ status=VerificationStatus.VALID,
163
+ message="API key is valid",
164
+ api_key_env_var=env_var,
165
+ )
166
+
167
+ except anthropic.AuthenticationError as e:
168
+ return VerificationResult(
169
+ provider="anthropic",
170
+ status=VerificationStatus.INVALID,
171
+ message="Invalid API key",
172
+ api_key_env_var=env_var,
173
+ details={"error": str(e)},
174
+ )
175
+
176
+ except anthropic.RateLimitError as e:
177
+ return VerificationResult(
178
+ provider="anthropic",
179
+ status=VerificationStatus.RATE_LIMITED,
180
+ message="Rate limit exceeded (key may be valid but quota exhausted)",
181
+ api_key_env_var=env_var,
182
+ details={"error": str(e)},
183
+ )
184
+
185
+ except anthropic.APIConnectionError as e:
186
+ return VerificationResult(
187
+ provider="anthropic",
188
+ status=VerificationStatus.CONNECTION_ERROR,
189
+ message="Failed to connect to Anthropic API",
190
+ api_key_env_var=env_var,
191
+ details={"error": str(e)},
192
+ )
193
+
194
+ except Exception as e:
195
+ return VerificationResult(
196
+ provider="anthropic",
197
+ status=VerificationStatus.UNKNOWN_ERROR,
198
+ message=f"Unexpected error: {e}",
199
+ api_key_env_var=env_var,
200
+ details={"error": str(e), "error_type": type(e).__name__},
201
+ )
202
+
203
+
204
+ def verify_gemini_api_key(api_key: str | None = None) -> VerificationResult:
205
+ """Verify a Google Gemini API key by making a lightweight API call.
206
+
207
+ Args:
208
+ api_key: Optional API key to verify. If not provided, uses GOOGLE_API_KEY
209
+ or GEMINI_API_KEY env var.
210
+
211
+ Returns:
212
+ VerificationResult with status and details
213
+ """
214
+ env_vars = ["GOOGLE_API_KEY", "GEMINI_API_KEY"]
215
+ env_var_used = None
216
+
217
+ if api_key is None:
218
+ for env_var in env_vars:
219
+ api_key = os.getenv(env_var)
220
+ if api_key:
221
+ env_var_used = env_var
222
+ break
223
+
224
+ if not api_key:
225
+ return VerificationResult(
226
+ provider="gemini",
227
+ status=VerificationStatus.MISSING,
228
+ message="GOOGLE_API_KEY or GEMINI_API_KEY environment variable is not set",
229
+ api_key_env_var="GOOGLE_API_KEY or GEMINI_API_KEY",
230
+ )
231
+
232
+ try:
233
+ client = genai.Client(api_key=api_key)
234
+ # List models - lightweight operation that verifies auth
235
+ models = list(client.models.list())
236
+ model_count = len(models)
237
+
238
+ return VerificationResult(
239
+ provider="gemini",
240
+ status=VerificationStatus.VALID,
241
+ message="API key is valid",
242
+ api_key_env_var=env_var_used or "GOOGLE_API_KEY",
243
+ details={"models_available": model_count},
244
+ )
245
+
246
+ except google_exceptions.PermissionDenied as e:
247
+ return VerificationResult(
248
+ provider="gemini",
249
+ status=VerificationStatus.INVALID,
250
+ message="Invalid API key",
251
+ api_key_env_var=env_var_used or "GOOGLE_API_KEY",
252
+ details={"error": str(e)},
253
+ )
254
+
255
+ except google_exceptions.ResourceExhausted as e:
256
+ return VerificationResult(
257
+ provider="gemini",
258
+ status=VerificationStatus.RATE_LIMITED,
259
+ message="Rate limit exceeded (key may be valid but quota exhausted)",
260
+ api_key_env_var=env_var_used or "GOOGLE_API_KEY",
261
+ details={"error": str(e)},
262
+ )
263
+
264
+ except google_exceptions.GoogleAPICallError as e:
265
+ return VerificationResult(
266
+ provider="gemini",
267
+ status=VerificationStatus.CONNECTION_ERROR,
268
+ message="Failed to connect to Gemini API",
269
+ api_key_env_var=env_var_used or "GOOGLE_API_KEY",
270
+ details={"error": str(e)},
271
+ )
272
+
273
+ except Exception as e:
274
+ return VerificationResult(
275
+ provider="gemini",
276
+ status=VerificationStatus.UNKNOWN_ERROR,
277
+ message=f"Unexpected error: {e}",
278
+ api_key_env_var=env_var_used or "GOOGLE_API_KEY",
279
+ details={"error": str(e), "error_type": type(e).__name__},
280
+ )
281
+
282
+
283
+ def verify_openrouter_api_key(api_key: str | None = None) -> VerificationResult:
284
+ """Verify an OpenRouter API key by making a lightweight API call.
285
+
286
+ Args:
287
+ api_key: Optional API key to verify. If not provided, uses OPENROUTER_API_KEY env var.
288
+
289
+ Returns:
290
+ VerificationResult with status and details
291
+ """
292
+ env_var = "OPENROUTER_API_KEY"
293
+
294
+ if api_key is None:
295
+ api_key = os.getenv(env_var)
296
+
297
+ if not api_key:
298
+ return VerificationResult(
299
+ provider="openrouter",
300
+ status=VerificationStatus.MISSING,
301
+ message=f"{env_var} environment variable is not set",
302
+ api_key_env_var=env_var,
303
+ )
304
+
305
+ try:
306
+ # OpenRouter uses OpenAI-compatible API
307
+ client = openai.OpenAI(
308
+ api_key=api_key,
309
+ base_url="https://openrouter.ai/api/v1",
310
+ )
311
+ # List models to verify auth
312
+ models = client.models.list()
313
+ model_count = sum(1 for _ in models)
314
+
315
+ return VerificationResult(
316
+ provider="openrouter",
317
+ status=VerificationStatus.VALID,
318
+ message="API key is valid",
319
+ api_key_env_var=env_var,
320
+ details={"models_available": model_count},
321
+ )
322
+
323
+ except openai.AuthenticationError as e:
324
+ return VerificationResult(
325
+ provider="openrouter",
326
+ status=VerificationStatus.INVALID,
327
+ message="Invalid API key",
328
+ api_key_env_var=env_var,
329
+ details={"error": str(e)},
330
+ )
331
+
332
+ except openai.RateLimitError as e:
333
+ return VerificationResult(
334
+ provider="openrouter",
335
+ status=VerificationStatus.RATE_LIMITED,
336
+ message="Rate limit exceeded (key may be valid but quota exhausted)",
337
+ api_key_env_var=env_var,
338
+ details={"error": str(e)},
339
+ )
340
+
341
+ except openai.APIConnectionError as e:
342
+ return VerificationResult(
343
+ provider="openrouter",
344
+ status=VerificationStatus.CONNECTION_ERROR,
345
+ message="Failed to connect to OpenRouter API",
346
+ api_key_env_var=env_var,
347
+ details={"error": str(e)},
348
+ )
349
+
350
+ except Exception as e:
351
+ return VerificationResult(
352
+ provider="openrouter",
353
+ status=VerificationStatus.UNKNOWN_ERROR,
354
+ message=f"Unexpected error: {e}",
355
+ api_key_env_var=env_var,
356
+ details={"error": str(e), "error_type": type(e).__name__},
357
+ )
358
+
359
+
360
+ def verify_ollama_connection(base_url: str | None = None) -> VerificationResult:
361
+ """Verify Ollama is running and accessible.
362
+
363
+ Args:
364
+ base_url: Optional base URL for Ollama server. Defaults to http://localhost:11434.
365
+
366
+ Returns:
367
+ VerificationResult with status and details
368
+ """
369
+ if base_url is None:
370
+ base_url = os.getenv("OLLAMA_HOST", "http://localhost:11434")
371
+
372
+ try:
373
+ # Ensure base_url ends with /v1 for OpenAI-compatible API
374
+ api_base = base_url.rstrip("/")
375
+ if not api_base.endswith("/v1"):
376
+ api_base = f"{api_base}/v1"
377
+
378
+ client = openai.OpenAI(
379
+ api_key="ollama", # Dummy key, not needed for Ollama
380
+ base_url=api_base,
381
+ )
382
+
383
+ # List models to verify connection
384
+ models = client.models.list()
385
+ model_names = [m.id for m in models]
386
+
387
+ return VerificationResult(
388
+ provider="ollama",
389
+ status=VerificationStatus.VALID,
390
+ message="Ollama is running and accessible",
391
+ details={"available_models": model_names, "base_url": base_url},
392
+ )
393
+
394
+ except openai.APIConnectionError as e:
395
+ return VerificationResult(
396
+ provider="ollama",
397
+ status=VerificationStatus.CONNECTION_ERROR,
398
+ message=f"Cannot connect to Ollama server at {base_url}. Is Ollama running?",
399
+ details={"error": str(e), "base_url": base_url},
400
+ )
401
+
402
+ except Exception as e:
403
+ return VerificationResult(
404
+ provider="ollama",
405
+ status=VerificationStatus.UNKNOWN_ERROR,
406
+ message=f"Unexpected error: {e}",
407
+ details={"error": str(e), "error_type": type(e).__name__, "base_url": base_url},
408
+ )
409
+
410
+
411
+ def verify_provider_api_key(
412
+ provider: str,
413
+ api_key: str | None = None,
414
+ **kwargs,
415
+ ) -> VerificationResult:
416
+ """Verify an API key for a specific provider.
417
+
418
+ Args:
419
+ provider: Provider name (openai, anthropic, gemini, openrouter, ollama)
420
+ api_key: Optional API key to verify. If not provided, uses environment variables.
421
+ **kwargs: Additional provider-specific options (e.g., base_url for ollama)
422
+
423
+ Returns:
424
+ VerificationResult with status and details
425
+ """
426
+ provider = provider.lower()
427
+
428
+ # Dispatch to provider-specific verification functions
429
+ verifiers = {
430
+ "openai": lambda: verify_openai_api_key(api_key),
431
+ "anthropic": lambda: verify_anthropic_api_key(api_key),
432
+ "gemini": lambda: verify_gemini_api_key(api_key),
433
+ "openrouter": lambda: verify_openrouter_api_key(api_key),
434
+ "ollama": lambda: verify_ollama_connection(kwargs.get("base_url")),
435
+ }
436
+
437
+ if provider in verifiers:
438
+ return verifiers[provider]()
439
+
440
+ if provider in ("test", "override"):
441
+ return VerificationResult(
442
+ provider=provider,
443
+ status=VerificationStatus.NOT_APPLICABLE,
444
+ message="Test provider, no verification needed",
445
+ )
446
+
447
+ return VerificationResult(
448
+ provider=provider,
449
+ status=VerificationStatus.UNKNOWN_ERROR,
450
+ message=f"Unknown provider: {provider}",
451
+ )
452
+
453
+
454
+ def _get_providers_to_check(include_optional: bool = False) -> list[str]:
455
+ """Get list of providers to check based on configured API keys.
456
+
457
+ Args:
458
+ include_optional: If True, include providers without keys set.
459
+
460
+ Returns:
461
+ List of provider names to verify.
462
+ """
463
+ providers_to_check = []
464
+ for provider, env_vars in PROVIDER_API_KEY_MAP.items():
465
+ if provider in ("test", "override"):
466
+ continue
467
+
468
+ if not env_vars:
469
+ # Ollama - always include if checking optional
470
+ if include_optional:
471
+ providers_to_check.append(provider)
472
+ continue
473
+
474
+ # Check if any env var is set
475
+ has_key = any(os.getenv(env_var) for env_var in env_vars)
476
+ if has_key or include_optional:
477
+ providers_to_check.append(provider)
478
+
479
+ return providers_to_check
480
+
481
+
482
+ def verify_all_api_keys(
483
+ providers: list[str] | None = None,
484
+ include_optional: bool = False,
485
+ ) -> dict[str, VerificationResult]:
486
+ """Verify API keys for multiple providers.
487
+
488
+ Args:
489
+ providers: List of providers to verify. If None, verifies all configured providers
490
+ (those with API keys set in environment).
491
+ include_optional: If True and providers is None, also check providers without
492
+ keys set to show which are missing.
493
+
494
+ Returns:
495
+ Dictionary mapping provider names to VerificationResult
496
+ """
497
+ if providers is None:
498
+ providers = _get_providers_to_check(include_optional)
499
+
500
+ results = {}
501
+ for provider in providers:
502
+ results[provider] = verify_provider_api_key(provider)
503
+
504
+ return results
505
+
506
+
507
+ async def verify_provider_api_key_async(
508
+ provider: str,
509
+ api_key: str | None = None,
510
+ **kwargs,
511
+ ) -> VerificationResult:
512
+ """Async version of verify_provider_api_key.
513
+
514
+ Runs the synchronous verification in a thread pool to avoid blocking.
515
+ """
516
+ return await asyncio.to_thread(verify_provider_api_key, provider, api_key, **kwargs)
517
+
518
+
519
+ async def verify_all_api_keys_async(
520
+ providers: list[str] | None = None,
521
+ include_optional: bool = False,
522
+ ) -> dict[str, VerificationResult]:
523
+ """Async version of verify_all_api_keys.
524
+
525
+ Verifies all providers concurrently for faster results.
526
+ """
527
+ if providers is None:
528
+ providers = _get_providers_to_check(include_optional)
529
+
530
+ # Run all verifications concurrently
531
+ tasks = [verify_provider_api_key_async(provider) for provider in providers]
532
+ results_list = await asyncio.gather(*tasks)
533
+
534
+ return dict(zip(providers, results_list, strict=True))