stratifyai 0.1.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 (57) hide show
  1. cli/__init__.py +5 -0
  2. cli/stratifyai_cli.py +1753 -0
  3. stratifyai/__init__.py +113 -0
  4. stratifyai/api_key_helper.py +372 -0
  5. stratifyai/caching.py +279 -0
  6. stratifyai/chat/__init__.py +54 -0
  7. stratifyai/chat/builder.py +366 -0
  8. stratifyai/chat/stratifyai_anthropic.py +194 -0
  9. stratifyai/chat/stratifyai_bedrock.py +200 -0
  10. stratifyai/chat/stratifyai_deepseek.py +194 -0
  11. stratifyai/chat/stratifyai_google.py +194 -0
  12. stratifyai/chat/stratifyai_grok.py +194 -0
  13. stratifyai/chat/stratifyai_groq.py +195 -0
  14. stratifyai/chat/stratifyai_ollama.py +201 -0
  15. stratifyai/chat/stratifyai_openai.py +209 -0
  16. stratifyai/chat/stratifyai_openrouter.py +201 -0
  17. stratifyai/chunking.py +158 -0
  18. stratifyai/client.py +292 -0
  19. stratifyai/config.py +1273 -0
  20. stratifyai/cost_tracker.py +257 -0
  21. stratifyai/embeddings.py +245 -0
  22. stratifyai/exceptions.py +91 -0
  23. stratifyai/models.py +59 -0
  24. stratifyai/providers/__init__.py +5 -0
  25. stratifyai/providers/anthropic.py +330 -0
  26. stratifyai/providers/base.py +183 -0
  27. stratifyai/providers/bedrock.py +634 -0
  28. stratifyai/providers/deepseek.py +39 -0
  29. stratifyai/providers/google.py +39 -0
  30. stratifyai/providers/grok.py +39 -0
  31. stratifyai/providers/groq.py +39 -0
  32. stratifyai/providers/ollama.py +43 -0
  33. stratifyai/providers/openai.py +344 -0
  34. stratifyai/providers/openai_compatible.py +372 -0
  35. stratifyai/providers/openrouter.py +39 -0
  36. stratifyai/py.typed +2 -0
  37. stratifyai/rag.py +381 -0
  38. stratifyai/retry.py +185 -0
  39. stratifyai/router.py +643 -0
  40. stratifyai/summarization.py +179 -0
  41. stratifyai/utils/__init__.py +11 -0
  42. stratifyai/utils/bedrock_validator.py +136 -0
  43. stratifyai/utils/code_extractor.py +327 -0
  44. stratifyai/utils/csv_extractor.py +197 -0
  45. stratifyai/utils/file_analyzer.py +192 -0
  46. stratifyai/utils/json_extractor.py +219 -0
  47. stratifyai/utils/log_extractor.py +267 -0
  48. stratifyai/utils/model_selector.py +324 -0
  49. stratifyai/utils/provider_validator.py +442 -0
  50. stratifyai/utils/token_counter.py +186 -0
  51. stratifyai/vectordb.py +344 -0
  52. stratifyai-0.1.0.dist-info/METADATA +263 -0
  53. stratifyai-0.1.0.dist-info/RECORD +57 -0
  54. stratifyai-0.1.0.dist-info/WHEEL +5 -0
  55. stratifyai-0.1.0.dist-info/entry_points.txt +2 -0
  56. stratifyai-0.1.0.dist-info/licenses/LICENSE +21 -0
  57. stratifyai-0.1.0.dist-info/top_level.txt +2 -0
stratifyai/__init__.py ADDED
@@ -0,0 +1,113 @@
1
+ """StratifyAI - Unified Intelligence Across Every Model Layer.
2
+
3
+ A production-ready Python module providing a unified, abstracted interface for
4
+ accessing multiple frontier LLM providers through a consistent API.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ from .caching import (
10
+ ResponseCache,
11
+ cache_response,
12
+ clear_cache,
13
+ generate_cache_key,
14
+ get_cache_stats,
15
+ )
16
+ from .client import LLMClient, ProviderType
17
+ from .exceptions import (
18
+ AuthenticationError,
19
+ BudgetExceededError,
20
+ InsufficientBalanceError,
21
+ InvalidModelError,
22
+ InvalidProviderError,
23
+ LLMAbstractionError,
24
+ MaxRetriesExceededError,
25
+ ProviderAPIError,
26
+ ProviderError,
27
+ RateLimitError,
28
+ ValidationError,
29
+ )
30
+ from .models import ChatRequest, ChatResponse, Message, Usage
31
+ from .providers.base import BaseProvider
32
+ from .providers.openai import OpenAIProvider
33
+ from .cost_tracker import CostTracker, CostEntry
34
+ from .retry import RetryConfig, with_retry
35
+ from .providers.anthropic import AnthropicProvider
36
+ from .providers.google import GoogleProvider
37
+ from .providers.deepseek import DeepSeekProvider
38
+ from .providers.groq import GroqProvider
39
+ from .providers.grok import GrokProvider
40
+ from .providers.ollama import OllamaProvider
41
+ from .providers.openrouter import OpenRouterProvider
42
+ from .providers.bedrock import BedrockProvider
43
+ from .router import Router, RoutingStrategy, ModelMetadata
44
+ from .embeddings import (
45
+ EmbeddingProvider,
46
+ EmbeddingResult,
47
+ OpenAIEmbeddingProvider,
48
+ create_embedding_provider,
49
+ )
50
+ from .vectordb import VectorDBClient, SearchResult
51
+ from .rag import RAGClient, RAGResponse, IndexingResult
52
+
53
+ __all__ = [
54
+ # Core client
55
+ "LLMClient",
56
+ "ProviderType",
57
+ # Data models
58
+ "Message",
59
+ "ChatRequest",
60
+ "ChatResponse",
61
+ "Usage",
62
+ # Providers
63
+ "BaseProvider",
64
+ "OpenAIProvider",
65
+ "AnthropicProvider",
66
+ "GoogleProvider",
67
+ "DeepSeekProvider",
68
+ "GroqProvider",
69
+ "GrokProvider",
70
+ "OllamaProvider",
71
+ "OpenRouterProvider",
72
+ "BedrockProvider",
73
+ # Caching
74
+ "ResponseCache",
75
+ "cache_response",
76
+ "generate_cache_key",
77
+ "get_cache_stats",
78
+ "clear_cache",
79
+ # Cost Tracking
80
+ "CostTracker",
81
+ "CostEntry",
82
+ # Retry
83
+ "RetryConfig",
84
+ "with_retry",
85
+ # Router
86
+ "Router",
87
+ "RoutingStrategy",
88
+ "ModelMetadata",
89
+ # Embeddings
90
+ "EmbeddingProvider",
91
+ "EmbeddingResult",
92
+ "OpenAIEmbeddingProvider",
93
+ "create_embedding_provider",
94
+ # Vector Database
95
+ "VectorDBClient",
96
+ "SearchResult",
97
+ # RAG
98
+ "RAGClient",
99
+ "RAGResponse",
100
+ "IndexingResult",
101
+ # Exceptions
102
+ "LLMAbstractionError",
103
+ "ProviderError",
104
+ "InvalidProviderError",
105
+ "ProviderAPIError",
106
+ "AuthenticationError",
107
+ "InsufficientBalanceError",
108
+ "RateLimitError",
109
+ "InvalidModelError",
110
+ "BudgetExceededError",
111
+ "MaxRetriesExceededError",
112
+ "ValidationError",
113
+ ]
@@ -0,0 +1,372 @@
1
+ """API key management and validation helpers."""
2
+
3
+ import os
4
+ from typing import Dict, Optional, Tuple
5
+ from pathlib import Path
6
+
7
+
8
+ class APIKeyHelper:
9
+ """Helper class for managing API keys with user-friendly error messages."""
10
+
11
+ # Map of provider names to their environment variable keys
12
+ PROVIDER_ENV_KEYS: Dict[str, str] = {
13
+ "openai": "OPENAI_API_KEY",
14
+ "anthropic": "ANTHROPIC_API_KEY",
15
+ "google": "GOOGLE_API_KEY",
16
+ "deepseek": "DEEPSEEK_API_KEY",
17
+ "groq": "GROQ_API_KEY",
18
+ "grok": "GROK_API_KEY",
19
+ "openrouter": "OPENROUTER_API_KEY",
20
+ "ollama": "OLLAMA_API_KEY",
21
+ "bedrock": "AWS_BEARER_TOKEN_BEDROCK", # Bedrock bearer token (or AWS_ACCESS_KEY_ID)
22
+ }
23
+
24
+ # Map of provider names to their API key signup URLs
25
+ PROVIDER_SIGNUP_URLS: Dict[str, str] = {
26
+ "openai": "https://platform.openai.com/api-keys",
27
+ "anthropic": "https://console.anthropic.com/settings/keys",
28
+ "google": "https://makersuite.google.com/app/apikey",
29
+ "deepseek": "https://platform.deepseek.com/api-docs/",
30
+ "groq": "https://console.groq.com/keys",
31
+ "grok": "https://x.ai/api",
32
+ "openrouter": "https://openrouter.ai/keys",
33
+ "ollama": "https://ollama.ai/download",
34
+ "bedrock": "https://docs.aws.amazon.com/bedrock/",
35
+ }
36
+
37
+ # Friendly provider names for error messages
38
+ PROVIDER_FRIENDLY_NAMES: Dict[str, str] = {
39
+ "openai": "OpenAI",
40
+ "anthropic": "Anthropic",
41
+ "google": "Google Gemini",
42
+ "deepseek": "DeepSeek",
43
+ "groq": "Groq",
44
+ "grok": "Grok (X.AI)",
45
+ "openrouter": "OpenRouter",
46
+ "ollama": "Ollama",
47
+ "bedrock": "AWS Bedrock",
48
+ }
49
+
50
+ @classmethod
51
+ def get_api_key(
52
+ cls,
53
+ provider: str,
54
+ api_key: Optional[str] = None
55
+ ) -> Optional[str]:
56
+ """
57
+ Get API key for a provider from parameter or environment.
58
+
59
+ Args:
60
+ provider: Provider name (e.g., "openai", "anthropic")
61
+ api_key: Optional API key to use instead of environment variable
62
+
63
+ Returns:
64
+ API key string or None if not found
65
+ """
66
+ # Use provided key if available
67
+ if api_key:
68
+ return api_key
69
+
70
+ # Get from environment
71
+ env_key = cls.PROVIDER_ENV_KEYS.get(provider)
72
+ if not env_key:
73
+ return None
74
+
75
+ return os.getenv(env_key)
76
+
77
+ @classmethod
78
+ def validate_api_key(
79
+ cls,
80
+ provider: str,
81
+ api_key: Optional[str] = None
82
+ ) -> Tuple[bool, Optional[str]]:
83
+ """
84
+ Validate that an API key is available for a provider.
85
+
86
+ Args:
87
+ provider: Provider name
88
+ api_key: Optional API key to validate
89
+
90
+ Returns:
91
+ Tuple of (is_valid, error_message)
92
+ If valid, error_message is None
93
+ If invalid, error_message contains helpful guidance
94
+ """
95
+ key = cls.get_api_key(provider, api_key)
96
+
97
+ if key:
98
+ return True, None
99
+
100
+ # Generate helpful error message
101
+ friendly_name = cls.PROVIDER_FRIENDLY_NAMES.get(provider, provider)
102
+ env_key = cls.PROVIDER_ENV_KEYS.get(provider, "API_KEY")
103
+ signup_url = cls.PROVIDER_SIGNUP_URLS.get(provider)
104
+
105
+ # Special handling for Bedrock (multiple auth methods)
106
+ if provider == "bedrock":
107
+ error_parts = [
108
+ f"❌ Missing API key for {friendly_name}",
109
+ "",
110
+ "AWS Bedrock supports multiple authentication methods:",
111
+ "",
112
+ "Option 1: Bearer token (simplest):",
113
+ f" export AWS_BEARER_TOKEN_BEDROCK=your-token-here",
114
+ "",
115
+ "Option 2: Access key + secret key:",
116
+ " export AWS_ACCESS_KEY_ID=your-access-key",
117
+ " export AWS_SECRET_ACCESS_KEY=your-secret-key",
118
+ "",
119
+ "Option 3: IAM roles (when running on AWS infrastructure)",
120
+ "Option 4: Configure ~/.aws/credentials",
121
+ "",
122
+ f"Get credentials: {signup_url}" if signup_url else "",
123
+ ]
124
+ else:
125
+ error_parts = [
126
+ f"❌ Missing API key for {friendly_name}",
127
+ "",
128
+ "To use this provider, you need to:",
129
+ f"1. Get an API key from: {signup_url}" if signup_url else "",
130
+ f"2. Set the {env_key} environment variable",
131
+ "",
132
+ "Quick setup:",
133
+ f" export {env_key}=your-api-key-here",
134
+ "",
135
+ "Or add to .env file:",
136
+ f" {env_key}=your-api-key-here",
137
+ "",
138
+ "Alternative: Pass api_key parameter:",
139
+ f" client = LLMClient(provider='{provider}', api_key='your-key')",
140
+ ]
141
+
142
+ # Remove empty strings from parts that had no signup URL
143
+ error_message = "\n".join([p for p in error_parts if p])
144
+
145
+ return False, error_message
146
+
147
+ @classmethod
148
+ def check_available_providers(cls) -> Dict[str, bool]:
149
+ """
150
+ Check which providers have API keys configured.
151
+
152
+ Returns:
153
+ Dictionary mapping provider names to availability (True/False)
154
+ """
155
+ available = {}
156
+ for provider in cls.PROVIDER_ENV_KEYS.keys():
157
+ key = cls.get_api_key(provider)
158
+ available[provider] = key is not None and len(key) > 0
159
+
160
+ return available
161
+
162
+ @classmethod
163
+ def get_setup_instructions(cls) -> str:
164
+ """
165
+ Get comprehensive setup instructions for all providers.
166
+
167
+ Returns:
168
+ Formatted setup instructions string
169
+ """
170
+ available = cls.check_available_providers()
171
+
172
+ lines = [
173
+ "🔑 StratifyAI API Key Setup",
174
+ "=" * 50,
175
+ "",
176
+ "Available Providers:",
177
+ ]
178
+
179
+ for provider in sorted(cls.PROVIDER_ENV_KEYS.keys()):
180
+ is_available = available.get(provider, False)
181
+ status = "✓" if is_available else "✗"
182
+ friendly_name = cls.PROVIDER_FRIENDLY_NAMES.get(provider, provider)
183
+ env_key = cls.PROVIDER_ENV_KEYS.get(provider)
184
+
185
+ lines.append(f" [{status}] {friendly_name} ({env_key})")
186
+
187
+ lines.extend([
188
+ "",
189
+ "Setup Instructions:",
190
+ " 1. Copy .env.example to .env: cp .env.example .env",
191
+ " 2. Add your API keys to .env file",
192
+ " 3. Test with: python -m cli.stratifyai_cli chat -p openai -m gpt-4o-mini -t 'Hello'",
193
+ "",
194
+ "Get API Keys:",
195
+ ])
196
+
197
+ for provider in sorted(cls.PROVIDER_SIGNUP_URLS.keys()):
198
+ friendly_name = cls.PROVIDER_FRIENDLY_NAMES.get(provider, provider)
199
+ url = cls.PROVIDER_SIGNUP_URLS.get(provider)
200
+ lines.append(f" • {friendly_name}: {url}")
201
+
202
+ lines.extend([
203
+ "",
204
+ "Note: You only need keys for providers you plan to use.",
205
+ ])
206
+
207
+ return "\n".join(lines)
208
+
209
+ @classmethod
210
+ def suggest_alternative_providers(
211
+ cls,
212
+ original_provider: str
213
+ ) -> Optional[str]:
214
+ """
215
+ Suggest alternative providers that have API keys configured.
216
+
217
+ Args:
218
+ original_provider: The provider that failed
219
+
220
+ Returns:
221
+ Formatted suggestion string or None if no alternatives
222
+ """
223
+ available = cls.check_available_providers()
224
+
225
+ # Find available alternatives
226
+ alternatives = [
227
+ p for p in available.keys()
228
+ if available[p] and p != original_provider
229
+ ]
230
+
231
+ if not alternatives:
232
+ return None
233
+
234
+ friendly_alternatives = [
235
+ cls.PROVIDER_FRIENDLY_NAMES.get(p, p)
236
+ for p in alternatives
237
+ ]
238
+
239
+ suggestion = [
240
+ "",
241
+ "💡 Tip: You have API keys configured for these providers:",
242
+ " " + ", ".join(friendly_alternatives),
243
+ "",
244
+ "Try using one of them instead, or get an API key for",
245
+ f"{cls.PROVIDER_FRIENDLY_NAMES.get(original_provider, original_provider)}.",
246
+ ]
247
+
248
+ return "\n".join(suggestion)
249
+
250
+ @classmethod
251
+ def create_env_file_if_missing(cls) -> bool:
252
+ """
253
+ Create .env file from .env.example if it doesn't exist.
254
+
255
+ Returns:
256
+ True if created, False if already exists or creation failed
257
+ """
258
+ env_path = Path(".env")
259
+ env_example_path = Path(".env.example")
260
+
261
+ # Skip if .env already exists
262
+ if env_path.exists():
263
+ return False
264
+
265
+ # Skip if .env.example doesn't exist
266
+ if not env_example_path.exists():
267
+ return False
268
+
269
+ try:
270
+ # Copy .env.example to .env
271
+ content = env_example_path.read_text()
272
+ env_path.write_text(content)
273
+ return True
274
+ except Exception:
275
+ return False
276
+
277
+
278
+ # Convenience functions for common use cases
279
+
280
+ def get_api_key_or_error(provider: str, api_key: Optional[str] = None) -> str:
281
+ """
282
+ Get API key for a provider or raise helpful error.
283
+
284
+ Args:
285
+ provider: Provider name
286
+ api_key: Optional API key to use
287
+
288
+ Returns:
289
+ API key string
290
+
291
+ Raises:
292
+ ValueError: With helpful error message if key not found
293
+ """
294
+ is_valid, error_message = APIKeyHelper.validate_api_key(provider, api_key)
295
+
296
+ if not is_valid:
297
+ # Add suggestion for alternative providers
298
+ suggestion = APIKeyHelper.suggest_alternative_providers(provider)
299
+ if suggestion:
300
+ error_message += suggestion
301
+
302
+ raise ValueError(error_message)
303
+
304
+ return APIKeyHelper.get_api_key(provider, api_key)
305
+
306
+
307
+ def print_setup_instructions() -> None:
308
+ """Print API key setup instructions to console with rich formatting."""
309
+ from rich.console import Console
310
+ from rich.table import Table
311
+
312
+ console = Console()
313
+ available = APIKeyHelper.check_available_providers()
314
+
315
+ # Count configured providers
316
+ configured_count = sum(1 for v in available.values() if v)
317
+ total_count = len(available)
318
+
319
+ # API Key Status Table
320
+ status_table = Table(show_header=True, header_style="bold magenta", title="API Key Status")
321
+ status_table.add_column("Provider", style="cyan")
322
+ status_table.add_column("Status", justify="center")
323
+ status_table.add_column("Environment Variable", style="dim")
324
+
325
+ for provider in sorted(available.keys()):
326
+ is_available = available[provider]
327
+ status = "[green]✓ Configured[/green]" if is_available else "[yellow]⚠ Missing[/yellow]"
328
+ friendly_name = APIKeyHelper.PROVIDER_FRIENDLY_NAMES.get(provider, provider)
329
+ env_key = APIKeyHelper.PROVIDER_ENV_KEYS.get(provider, "N/A")
330
+
331
+ status_table.add_row(friendly_name, status, env_key)
332
+
333
+ console.print(status_table)
334
+
335
+ # Summary
336
+ if configured_count == 0:
337
+ console.print(f"\n[yellow]⚠ No providers configured[/yellow]")
338
+ elif configured_count == total_count:
339
+ console.print(f"\n[green]✓ All {total_count} providers configured![/green]")
340
+ else:
341
+ console.print(f"\n[cyan]{configured_count}/{total_count} providers configured[/cyan]")
342
+
343
+ # Provider signup URLs table
344
+ console.print("\n[bold magenta]Available Providers[/bold magenta]")
345
+
346
+ providers_table = Table(show_header=True, header_style="bold magenta")
347
+ providers_table.add_column("Provider", style="cyan")
348
+ providers_table.add_column("Get API Key", style="blue")
349
+
350
+ for provider in sorted(APIKeyHelper.PROVIDER_SIGNUP_URLS.keys()):
351
+ friendly_name = APIKeyHelper.PROVIDER_FRIENDLY_NAMES.get(provider, provider)
352
+ url = APIKeyHelper.PROVIDER_SIGNUP_URLS.get(provider)
353
+ providers_table.add_row(friendly_name, url)
354
+
355
+ console.print(providers_table)
356
+
357
+ # Help tip
358
+ console.print("\n[dim]💡 Tip: You only need to configure providers you plan to use[/dim]")
359
+
360
+
361
+ def check_provider_available(provider: str) -> bool:
362
+ """
363
+ Check if a provider has an API key configured.
364
+
365
+ Args:
366
+ provider: Provider name
367
+
368
+ Returns:
369
+ True if API key is available, False otherwise
370
+ """
371
+ available = APIKeyHelper.check_available_providers()
372
+ return available.get(provider, False)