/ my_script.py
my_script.py
1 #!/usr/bin/env python3 2 """ 3 AI FREEDOM ORCHESTRATOR v3.0 4 With OpenAI integration and all free APIs 5 """ 6 7 import os 8 import sys 9 import json 10 import asyncio 11 import aiohttp 12 import random 13 import time 14 from datetime import datetime 15 from typing import List, Dict, Optional, Any 16 from dataclasses import dataclass, asdict 17 from enum import Enum 18 import logging 19 20 # Configure logging 21 logging.basicConfig( 22 level=logging.INFO, 23 format='%(asctime)s - %(levelname)s - %(message)s' 24 ) 25 logger = logging.getLogger(__name__) 26 27 # ========== CONFIGURATION ========== 28 class APISource(Enum): 29 OPENAI = "openai" # Paid, most powerful 30 TOGETHER = "together" # $25 free credits, uncensored 31 GROQ = "groq" # Fast inference, free 32 GOOGLE = "google" # 1M tokens/month free (Using Gemini API) 33 OPENROUTER = "openrouter" # Uncensored models, free tier 34 HUGGINGFACE = "huggingface" # 30K tokens/month free 35 DEEPSEEK = "deepseek" # Completely free 36 LOCAL = "local" # Uncensored, 100% free (Placeholder for local LLM e.g. Ollama) 37 38 # API rate limits (requests per minute) 39 RATE_LIMITS = { 40 APISource.OPENAI: 3500, # Tokens per minute for gpt-3.5-turbo 41 APISource.TOGETHER: 30, 42 APISource.GROQ: 30, 43 APISource.GOOGLE: 60, 44 APISource.OPENROUTER: 20, 45 APISource.HUGGINGFACE: 10, 46 APISource.DEEPSEEK: 30, 47 APISource.LOCAL: 100, # High local limit 48 } 49 50 # Model configurations with ALL models 51 # NOTE: The HuggingFace URL is usually specific to the model. Using the Mixtral one as default. 52 MODEL_CONFIGS = { 53 APISource.OPENAI: { 54 "models": [ 55 "gpt-3.5-turbo", # Fast, cheap 56 "gpt-4", # Most powerful 57 "gpt-4-turbo-preview", # Latest 58 "gpt-4-32k", # Large context 59 ], 60 "default_model": "gpt-3.5-turbo", 61 "url": "https://api.openai.com/v1/chat/completions", 62 "headers_template": {"Authorization": "Bearer {key}"} 63 }, 64 APISource.TOGETHER: { 65 "models": [ 66 "mistralai/Mixtral-8x7B-Instruct-v0.1", 67 "meta-llama/Llama-2-70b-chat-hf", 68 "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", 69 "Qwen/Qwen1.5-72B-Chat", 70 "codellama/CodeLlama-70b-Instruct-hf", 71 ], 72 "default_model": "mistralai/Mixtral-8x7B-Instruct-v0.1", 73 "url": "https://api.together.xyz/v1/chat/completions", 74 "headers_template": {"Authorization": "Bearer {key}"} 75 }, 76 APISource.GROQ: { 77 "models": ["mixtral-8x7b-32768", "llama2-70b-4096"], 78 "default_model": "mixtral-8x7b-32768", 79 # Groq uses OpenAI-compatible API 80 "url": "https://api.groq.com/openai/v1/chat/completions", 81 "headers_template": {"Authorization": "Bearer {key}"} 82 }, 83 APISource.GOOGLE: { 84 "models": ["gemini-pro"], 85 "default_model": "gemini-pro", 86 # Gemini uses API key in URL, not Header 87 "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key={key}", 88 "headers_template": {} 89 }, 90 APISource.OPENROUTER: { 91 "models": [ 92 "mistralai/mistral-7b-instruct:free", 93 "google/gemini-pro:free", 94 "anthropic/claude-3-haiku:beta", 95 ], 96 "default_model": "mistralai/mistral-7b-instruct:free", 97 # OpenRouter uses OpenAI-compatible API 98 "url": "https://openrouter.ai/api/v1/chat/completions", 99 "headers_template": { 100 "Authorization": "Bearer {key}", 101 "HTTP-Referer": "https://github.com/user/repo", 102 "X-Title": "AI Freedom Orchestrator" 103 } 104 }, 105 APISource.HUGGINGFACE: { 106 "models": [ 107 "mistralai/Mixtral-8x7B-Instruct-v0.1", 108 "google/flan-t5-xxl", 109 "bigscience/bloom", 110 ], 111 "default_model": "mistralai/Mixtral-8x7B-Instruct-v0.1", 112 # HuggingFace API URL should be dynamic based on model, but is hardcoded for simplicity. 113 "url": "https://api-inference.huggingface.co/models/{model}", 114 "headers_template": {"Authorization": "Bearer {key}"} 115 }, 116 APISource.DEEPSEEK: { 117 "models": ["deepseek-chat"], 118 "default_model": "deepseek-chat", 119 # Deepseek uses OpenAI-compatible API 120 "url": "https://api.deepseek.com/v1/chat/completions", 121 "headers_template": {"Authorization": "Bearer {key}"} 122 }, 123 APISource.LOCAL: { 124 "models": ["llama2-7b-chat"], # Example Ollama model 125 "default_model": "llama2-7b-chat", 126 # Placeholder for local API like Ollama or LM Studio 127 "url": "http://localhost:11434/v1/chat/completions", 128 "headers_template": {} 129 } 130 } 131 132 # ========== DATA CLASSES ========== 133 @dataclass 134 class APIKey: 135 source: APISource 136 key: str 137 enabled: bool = True 138 last_used: float = 0 139 failure_count: int = 0 140 credits_remaining: Optional[float] = None 141 142 # Allow Enum in asdict/JSON serialization 143 def to_dict(self): 144 return {**asdict(self), 'source': self.source.value} 145 146 @dataclass 147 class APIResponse: 148 source: APISource 149 content: str 150 latency: float 151 success: bool 152 error: Optional[str] = None 153 tokens_used: int = 0 154 cost: float = 0 155 model: str = "" 156 157 # Allow Enum in asdict/JSON serialization 158 def to_dict(self): 159 return {**asdict(self), 'source': self.source.value} 160 161 @dataclass 162 class QueryResult: 163 query: str 164 responses: List[APIResponse] 165 best_response: Optional[APIResponse] = None 166 synthesis: Optional[str] = None 167 processing_time: float = 0 168 total_cost: float = 0 169 170 # Allow nested Enum/Dataclass in JSON serialization 171 def to_dict(self): 172 data = asdict(self) 173 data['responses'] = [r.to_dict() for r in self.responses] 174 if self.best_response: 175 data['best_response'] = self.best_response.to_dict() 176 return data 177 178 # ========== RATE LIMITER ========== 179 class RateLimiter: 180 """Manages API rate limits with exponential backoff""" 181 182 def __init__(self): 183 self.requests = {} 184 self.semaphores = {} 185 186 # Initialize semaphores based on rate limits 187 for source, limit in RATE_LIMITS.items(): 188 # Use 1 request per 60 seconds for the semaphore, actual rate limiting 189 # is done with the `requests` list for per-minute granularity 190 self.semaphores[source] = asyncio.Semaphore(limit) 191 self.requests[source] = [] 192 193 async def acquire(self, source: APISource): 194 """Acquire permission to make a request with rate limiting""" 195 semaphore = self.semaphores.get(source) 196 if semaphore: 197 await semaphore.acquire() 198 199 # Clean old requests 200 now = time.time() 201 self.requests[source] = [req_time for req_time in self.requests[source] 202 if now - req_time < 60] 203 204 # Check if we need to wait for per-minute limit 205 limit = RATE_LIMITS.get(source) 206 if limit is not None and len(self.requests[source]) >= limit: 207 # Wait for the oldest request to age out of the 60-second window 208 wait_time = 60 - (now - min(self.requests[source])) 209 if wait_time > 0: 210 # Add a small random jitter to avoid thundering herd problem 211 await asyncio.sleep(wait_time + random.uniform(0, 1)) 212 213 # Update timestamp after wait 214 self.requests[source].append(time.time()) 215 216 def release(self, source: APISource): 217 """Release the semaphore after request completes""" 218 semaphore = self.semaphores.get(source) 219 if semaphore: 220 semaphore.release() 221 222 # ========== API MANAGER ========== 223 class APIManager: 224 """Manages all API connections with intelligent routing""" 225 226 def __init__(self): 227 self.keys = self._load_api_keys() 228 self.rate_limiter = RateLimiter() 229 self.session: Optional[aiohttp.ClientSession] = None 230 self.circuit_breakers: Dict[APISource, List[Any]] = {} 231 232 def _load_api_keys(self) -> Dict[APISource, APIKey]: 233 """Load API keys from environment variables and hardcoded defaults""" 234 keys = {} 235 236 # OpenAI (from environment) 237 openai_key = os.getenv("OPENAI_API_KEY") 238 if openai_key: 239 keys[APISource.OPENAI] = APIKey( 240 APISource.OPENAI, 241 openai_key, 242 credits_remaining=None # OpenAI is pay-as-you-go 243 ) 244 245 # Other API Keys (Placeholder values) 246 # NOTE: REPLACE THESE PLACEHOLDERS WITH YOUR ACTUAL KEYS! 247 keys[APISource.TOGETHER] = APIKey( 248 APISource.TOGETHER, 249 "TOGETHER_API_KEY_PLACEHOLDER", 250 credits_remaining=25.0 251 ) 252 253 keys[APISource.GROQ] = APIKey( 254 APISource.GROQ, 255 "GROQ_API_KEY_PLACEHOLDER" 256 ) 257 258 keys[APISource.GOOGLE] = APIKey( 259 APISource.GOOGLE, 260 "GOOGLE_API_KEY_PLACEHOLDER" 261 ) 262 263 keys[APISource.HUGGINGFACE] = APIKey( 264 APISource.HUGGINGFACE, 265 "HUGGINGFACE_API_KEY_PLACEHOLDER" 266 ) 267 268 keys[APISource.OPENROUTER] = APIKey( 269 APISource.OPENROUTER, 270 "OPENROUTER_API_KEY_PLACEHOLDER" 271 ) 272 273 # Check for other keys from environment variables 274 env_keys = { 275 APISource.DEEPSEEK: os.getenv("DEEPSEEK_API_KEY"), 276 APISource.LOCAL: os.getenv("LOCAL_API_KEY") # Or check if local server is running 277 } 278 279 for source, key in env_keys.items(): 280 if key: 281 keys[source] = APIKey(source, key) 282 283 # Add a default key for LOCAL if not found (assuming an Ollama-like local setup) 284 if APISource.LOCAL not in keys: 285 # In a real setup, you'd check for a running local server instead of an environment key 286 keys[APISource.LOCAL] = APIKey( 287 APISource.LOCAL, 288 "LOCAL_SERVER_KEY", # This key might not be strictly needed for a local server 289 enabled=False # Disable by default unless a real key/check is implemented 290 ) 291 292 293 logger.info(f"Loaded {len(keys)} API sources") 294 295 # Log status 296 for source, key in keys.items(): 297 status = "š° Paid" if source == APISource.OPENAI else "š Free" 298 status = f"{status} | {'ENABLED' if key.enabled else 'DISABLED'}" 299 if key.credits_remaining is not None: 300 status += f" (${key.credits_remaining:.2f} credits)" 301 302 # Use 'Found' for keys that came from the environment or are explicitly enabled 303 key_status = "FOUND" if key.enabled and source != APISource.LOCAL else "SKIPPED/DISABLED" 304 305 logger.info(f"[{key_status}] {status}: {source.value.upper()}") 306 307 return keys 308 309 async def __aenter__(self): 310 self.session = aiohttp.ClientSession( 311 timeout=aiohttp.ClientTimeout(total=30), 312 connector=aiohttp.TCPConnector(limit=100) 313 ) 314 return self 315 316 async def __aexit__(self, exc_type, exc_val, exc_tb): 317 if self.session: 318 await self.session.close() 319 320 def _is_circuit_open(self, source: APISource) -> bool: 321 """Check if circuit breaker is open for an API""" 322 if source not in self.circuit_breakers: 323 return False 324 325 # [failures, last_failure_time] 326 failures, last_failure = self.circuit_breakers[source] 327 # Open circuit if > 5 consecutive failures in the last 5 minutes (300 seconds) 328 if failures > 5 and time.time() - last_failure < 300: 329 return True 330 331 return False 332 333 def _record_failure(self, source: APISource): 334 """Record API failure for circuit breaker""" 335 if source not in self.circuit_breakers or time.time() - self.circuit_breakers[source][1] > 300: 336 # Start new count if first failure or last failure was long ago 337 self.circuit_breakers[source] = [1, time.time()] 338 else: 339 failures, _ = self.circuit_breakers[source] 340 self.circuit_breakers[source] = [failures + 1, time.time()] 341 342 def _record_success(self, source: APISource): 343 """Reset circuit breaker on success""" 344 if source in self.circuit_breakers: 345 self.circuit_breakers[source] = [0, time.time()] 346 347 def _calculate_cost(self, source: APISource, tokens: int, model: str = "") -> float: 348 """Calculate approximate cost for API call""" 349 # Costs per 1K tokens (USD) - simplified, assuming all tokens are output tokens 350 # In a real app, you'd track prompt_tokens and completion_tokens separately 351 cost_per_1k = { 352 APISource.OPENAI: { 353 "gpt-3.5-turbo": 0.0015, # $1.5 per 1M tokens 354 "gpt-4": 0.03, # $30 per 1M tokens 355 "gpt-4-turbo-preview": 0.01, # $10 per 1M tokens 356 "gpt-4-32k": 0.06, # $60 per 1M tokens 357 "default": 0.0015 358 }, 359 APISource.TOGETHER: 0.0006, # Example: $0.60 per 1M tokens 360 APISource.GROQ: 0.0000, # Free tier 361 APISource.GOOGLE: 0.0000, # Free tier 362 APISource.OPENROUTER: 0.0000, # Free tier 363 APISource.HUGGINGFACE: 0.0000, # Free tier 364 APISource.DEEPSEEK: 0.0000, # Free tier 365 APISource.LOCAL: 0.0000, # Free 366 } 367 368 if source == APISource.OPENAI: 369 cost_map = cost_per_1k[source] 370 cost_per_token = cost_map.get(model, cost_map["default"]) / 1000 371 else: 372 cost_per_1k_rate = cost_per_1k.get(source, 0.001) 373 cost_per_token = cost_per_1k_rate / 1000 374 375 return tokens * cost_per_token 376 377 async def query_api(self, source: APISource, prompt: str, 378 max_retries: int = 3, model_override: str = None) -> APIResponse: 379 """Query a specific API with retry logic""" 380 381 api_key = self.keys.get(source) 382 if not api_key or not api_key.enabled: 383 return APIResponse( 384 source=source, 385 content="", 386 latency=0, 387 success=False, 388 error=f"API {source.value} not available or disabled" 389 ) 390 391 if self._is_circuit_open(source): 392 return APIResponse( 393 source=source, 394 content="", 395 latency=0, 396 success=False, 397 error=f"Circuit breaker open for {source.value}" 398 ) 399 400 config = MODEL_CONFIGS.get(source) 401 if not config: 402 return APIResponse( 403 source=source, 404 content="", 405 latency=0, 406 success=False, 407 error=f"No configuration for {source.value}" 408 ) 409 410 # Select model 411 if model_override and model_override in config["models"]: 412 model = model_override 413 else: 414 model = config["default_model"] 415 416 start_time = time.time() 417 418 for attempt in range(max_retries): 419 try: 420 await self.rate_limiter.acquire(source) 421 422 headers = {"Content-Type": "application/json"} 423 for key, value in config["headers_template"].items(): 424 headers[key] = value.format(key=api_key.key) 425 426 payload = self._prepare_payload(source, prompt, model) 427 428 # Replace model placeholder in URL for HuggingFace (and others if needed) 429 url = config["url"].format(key=api_key.key, model=model) if "{key}" in config["url"] or "{model}" in config["url"] else config["url"] 430 431 # Ensure we have a session (should be guaranteed by __aenter__) 432 if not self.session: 433 raise Exception("AIOHTTP session not initialized.") 434 435 async with self.session.post(url, json=payload, headers=headers) as response: 436 response_text = await response.text() 437 438 if response.status == 200: 439 content, tokens_out = self._extract_response(source, response_text) 440 latency = time.time() - start_time 441 442 # Crude token calculation if API doesn't return usage 443 tokens_used = tokens_out if tokens_out > 0 else len(prompt.split()) + len(content.split()) 444 # Use a 1.3 factor for a slightly more accurate token estimate (if no usage data) 445 tokens_used = int(tokens_used * 1.3) 446 447 cost = self._calculate_cost(source, tokens_used, model) 448 449 # Update credits 450 if source == APISource.TOGETHER and api_key.credits_remaining is not None: 451 api_key.credits_remaining = max(0, api_key.credits_remaining - cost) 452 453 self._record_success(source) 454 api_key.last_used = time.time() 455 456 return APIResponse( 457 source=source, 458 content=content, 459 latency=latency, 460 success=True, 461 tokens_used=tokens_used, 462 cost=cost, 463 model=model 464 ) 465 else: 466 if response.status == 429: 467 wait_time = (2 ** attempt) + random.random() 468 logger.warning(f"Rate limited by {source.value}, waiting {wait_time:.1f}s") 469 await asyncio.sleep(wait_time) 470 continue 471 elif response.status in [401, 403]: # Unauthorized/Forbidden 472 api_key.enabled = False 473 return APIResponse( 474 source=source, 475 content="", 476 latency=time.time() - start_time, 477 success=False, 478 error=f"Invalid/Expired API key for {source.value} (Status: {response.status})" 479 ) 480 else: 481 self._record_failure(source) 482 error_msg = f"HTTP {response.status}: {response_text[:100]}" 483 484 if attempt < max_retries - 1: 485 await asyncio.sleep((2 ** attempt) + random.random()) 486 continue 487 else: 488 return APIResponse( 489 source=source, 490 content="", 491 latency=time.time() - start_time, 492 success=False, 493 error=error_msg 494 ) 495 496 except asyncio.TimeoutError: 497 logger.warning(f"Timeout for {source.value}, attempt {attempt + 1}") 498 if attempt < max_retries - 1: 499 await asyncio.sleep((2 ** attempt) + random.random()) 500 continue 501 502 except Exception as e: 503 logger.error(f"Error querying {source.value}: {str(e)}") 504 self._record_failure(source) 505 506 if attempt < max_retries - 1: 507 await asyncio.sleep((2 ** attempt) + random.random()) 508 continue 509 else: 510 return APIResponse( 511 source=source, 512 content="", 513 latency=time.time() - start_time, 514 success=False, 515 error=str(e) 516 ) 517 518 finally: 519 # Release the semaphore even if the request failed for a non-retryable reason 520 self.rate_limiter.release(source) 521 522 return APIResponse( 523 source=source, 524 content="", 525 latency=time.time() - start_time, 526 success=False, 527 error="All retries failed" 528 ) 529 530 def _prepare_payload(self, source: APISource, prompt: str, model: str) -> dict: 531 """Prepare API-specific payload""" 532 if source == APISource.GOOGLE: 533 return { 534 "contents": [{ 535 "parts": [{"text": prompt}] 536 }], 537 "config": { # Changed from generationConfig to config for modern usage 538 "maxOutputTokens": 2000, 539 "temperature": 0.7 540 } 541 } 542 elif source == APISource.HUGGINGFACE: 543 return { 544 "inputs": prompt, 545 "parameters": { 546 "max_new_tokens": 1000, 547 "temperature": 0.7, 548 "return_full_text": False 549 } 550 } 551 else: 552 # OpenAI-compatible format (OpenAI, Together, Groq, OpenRouter, Deepseek, Local) 553 return { 554 "model": model, 555 "messages": [{"role": "user", "content": prompt}], 556 "max_tokens": 2000, 557 "temperature": 0.7 558 } 559 560 def _extract_response(self, source: APISource, response_text: str) -> tuple[str, int]: 561 """Extract content and tokens_used from API response""" 562 try: 563 data = json.loads(response_text) 564 tokens_used = 0 565 566 if source == APISource.GOOGLE: 567 # Fixed deep nesting access for Gemini API 568 content = data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "") 569 570 # Gemini usage is usually under usageMetadata 571 usage = data.get("usageMetadata", {}) 572 tokens_used = usage.get("totalTokenCount", 0) 573 return content, tokens_used 574 575 elif source == APISource.HUGGINGFACE: 576 if isinstance(data, list) and data: 577 content = data[0].get("generated_text", "") 578 else: 579 content = data.get("generated_text", str(data)) 580 # HuggingFace doesn't always return token count in the standard inference API 581 return content, 0 582 583 # OpenAI-compatible APIs (OpenAI, Together, Groq, OpenRouter, Deepseek, Local) 584 else: 585 content = data.get("choices", [{}])[0].get("message", {}).get("content", "") 586 587 # Standard OpenAI usage object extraction 588 usage = data.get("usage", {}) 589 tokens_used = usage.get("total_tokens", 0) 590 return content, tokens_used 591 592 except json.JSONDecodeError: 593 logger.error(f"Failed to decode JSON from {source.value}: {response_text[:100]}...") 594 return response_text, 0 595 except Exception as e: 596 logger.error(f"Error during response extraction for {source.value}: {e}") 597 return "", 0 598 599 # ========== INTELLIGENT ORCHESTRATOR ========== 600 class AIFreedomOrchestrator: 601 """Intelligent orchestrator with cost-aware routing""" 602 603 def __init__(self, strategy: str = "balanced"): 604 """ 605 strategy options: 606 - "balanced": Mix of free and paid 607 - "free_only": Only use free APIs 608 - "premium": Prefer paid APIs (OpenAI) when available 609 - "uncensored": Prefer uncensored models (Together, Local, OpenRouter) 610 """ 611 # api_manager will be set in main() using async context manager 612 self.api_manager: APIManager 613 self.strategy = strategy 614 self.results_cache = {} 615 616 async def process_query(self, query: str, budget: float = 0.10) -> QueryResult: 617 """Process query with intelligent routing based on strategy""" 618 619 start_time = time.time() 620 621 # Check cache 622 cache_key = f"{query}_{self.strategy}_{budget}" 623 if cache_key in self.results_cache: 624 cached = self.results_cache[cache_key] 625 if time.time() - cached["timestamp"] < 300: # Cache lifetime of 5 minutes 626 logger.info(f"Cache hit for query: {query[:50]}...") 627 return cached["result"] 628 629 logger.info(f"Processing query with strategy: {self.strategy}") 630 631 # Select APIs based on strategy 632 enabled_apis = self._select_apis_for_strategy(budget) 633 634 if not enabled_apis: 635 logger.warning("No APIs available for selected strategy!") 636 return QueryResult( 637 query=query, 638 responses=[], 639 processing_time=time.time() - start_time 640 ) 641 642 # Process query 643 all_responses = await self._parallel_query_apis(enabled_apis, query) 644 645 # Select best response 646 best_response = self._select_best_response(all_responses) 647 648 # Synthesize if multiple good responses 649 synthesis = None 650 good_responses = [r for r in all_responses if r.success and r.content] 651 if len(good_responses) > 1: 652 synthesis = await self._synthesize_responses(query, good_responses) 653 654 total_cost = sum(r.cost for r in all_responses if r.success) 655 656 result = QueryResult( 657 query=query, 658 responses=all_responses, 659 best_response=best_response, 660 synthesis=synthesis, 661 processing_time=time.time() - start_time, 662 total_cost=total_cost 663 ) 664 665 self.results_cache[cache_key] = { 666 "result": result, 667 "timestamp": time.time() 668 } 669 670 return result 671 672 def _select_apis_for_strategy(self, budget: float) -> List[APISource]: 673 """Select APIs based on strategy and budget""" 674 675 all_apis = list(self.api_manager.keys.keys()) 676 677 # Filter out disabled keys first 678 available_apis = [api for api in all_apis if self.api_manager.keys[api].enabled] 679 680 if self.strategy == "free_only": 681 # Remove OpenAI (paid) 682 return [api for api in available_apis if api != APISource.OPENAI] 683 684 elif self.strategy == "premium": 685 # Start with OpenAI, then add free as fallback 686 enabled = [] 687 if APISource.OPENAI in available_apis: 688 enabled.append(APISource.OPENAI) 689 enabled.extend([api for api in available_apis if api != APISource.OPENAI]) 690 return enabled 691 692 elif self.strategy == "uncensored": 693 # Prefer uncensored models 694 uncensored_order = [ 695 APISource.TOGETHER, # Uncensored 696 APISource.LOCAL, # Uncensored 697 APISource.OPENROUTER, # Uncensored models 698 APISource.OPENAI, # Sometimes censored 699 APISource.GROQ, # Sometimes censored 700 APISource.DEEPSEEK, # Sometimes censored 701 APISource.HUGGINGFACE, # Sometimes censored 702 APISource.GOOGLE, # Heavily censored 703 ] 704 return [api for api in uncensored_order if api in available_apis] 705 706 else: # "balanced" - default 707 # Mix of paid and free 708 balanced_order = [ 709 APISource.OPENAI, # Paid but powerful 710 APISource.TOGETHER, # Free credits 711 APISource.GROQ, # Free and fast 712 APISource.GOOGLE, # Free 713 APISource.OPENROUTER, # Free 714 APISource.HUGGINGFACE, # Free 715 APISource.DEEPSEEK, # Free 716 APISource.LOCAL, # Free 717 ] 718 return [api for api in balanced_order if api in available_apis] 719 720 async def _parallel_query_apis(self, apis: List[APISource], query: str) -> List[APIResponse]: 721 """Query multiple APIs in parallel""" 722 723 # Ensure we don't query the same API key if it's disabled globally (already filtered) 724 tasks = [self.api_manager.query_api(api, query) for api in apis] 725 726 try: 727 # Execute all tasks concurrently 728 responses = await asyncio.gather(*tasks, return_exceptions=True) 729 730 processed = [] 731 for response in responses: 732 if isinstance(response, Exception): 733 # Exception during task execution (e.g., in _load_api_keys) 734 logger.error(f"Task failed during execution: {response}") 735 continue 736 if response: 737 processed.append(response) 738 739 return processed 740 741 except Exception as e: 742 logger.error(f"Parallel query failed: {e}") 743 return [] 744 745 async def _synthesize_responses(self, query: str, responses: List[APIResponse]) -> str: 746 """Synthesize multiple responses""" 747 748 # The list is filtered for good responses already, but check again 749 if not responses: 750 return "" 751 752 # Prepare synthesis prompt 753 sources_text = "\n\n".join([ 754 f"SOURCE: {resp.source.value.upper()} ({resp.model})\n" 755 f"CONTENT: {resp.content[:500]}..." 756 for resp in responses 757 ]) 758 759 synthesis_prompt = f"""Combine these responses into one comprehensive answer: 760 761 QUESTION: {query} 762 763 RESPONSES: 764 {sources_text} 765 766 Instructions: 767 1. Merge the best information from all sources 768 2. Resolve contradictions 769 3. Create a well-structured, comprehensive answer 770 4. Include practical insights 771 772 ANSWER:""" 773 774 # Use the most powerful available API for synthesis 775 synthesis_priority = [ 776 APISource.OPENAI, # Best for synthesis 777 APISource.TOGETHER, # Good alternative 778 APISource.GROQ, # Fast 779 APISource.GOOGLE, # Reliable 780 ] 781 782 for api in synthesis_priority: 783 api_key_obj = self.api_manager.keys.get(api) 784 if api_key_obj and api_key_obj.enabled: 785 # Use a more powerful model if available (e.g. gpt-4 for OpenAI) 786 model_for_synthesis = "gpt-4" if api == APISource.OPENAI and "gpt-4" in MODEL_CONFIGS[api]["models"] else None 787 response = await self.api_manager.query_api(api, synthesis_prompt, model_override=model_for_synthesis) 788 if response.success: 789 logger.info(f"Synthesis successful using {api.value.upper()}.") 790 return response.content 791 792 # Fallback to a simple concatenation if synthesis fails entirely 793 logger.warning("Synthesis failed, returning best single response.") 794 return self._select_best_response(responses).content if self._select_best_response(responses) else "" 795 796 def _select_best_response(self, responses: List[APIResponse]) -> Optional[APIResponse]: 797 """Select best response based on quality and cost""" 798 799 # Filter for successful, non-empty responses first 800 successful_responses = [resp for resp in responses if resp.success and resp.content.strip()] 801 802 if not successful_responses: 803 return responses[0] if responses else None 804 805 # Score each response 806 scored = [] 807 for resp in successful_responses: 808 score = 0 809 810 # Content quality: penalize short/empty and reward reasonable length 811 content_len = len(resp.content.strip()) 812 score += min(content_len / 100, 20) # Max 20 points for content length 813 814 # Speed: reward fast responses (fastest scores max 15) 815 if resp.latency < 2: 816 score += 15 817 elif resp.latency < 5: 818 score += 10 819 elif resp.latency < 10: 820 score += 5 821 822 # Source quality: based on model perceived power/reliability 823 source_scores = { 824 APISource.OPENAI: 25, # Highest quality 825 APISource.TOGETHER: 20, # Uncensored, powerful 826 APISource.LOCAL: 18, # Uncensored, fast if running well 827 APISource.GOOGLE: 16, # Reliable 828 APISource.GROQ: 14, # Fast 829 APISource.OPENROUTER: 12, # Uncensored 830 APISource.DEEPSEEK: 10, 831 APISource.HUGGINGFACE: 8, 832 } 833 score += source_scores.get(resp.source, 5) 834 835 # Cost efficiency: prioritize free/cheap 836 if resp.cost == 0: 837 score += 10 838 elif resp.cost < 0.001: 839 score += 5 840 elif resp.cost > 0.01: # Penalize very expensive calls 841 score -= 10 842 843 scored.append((score, resp)) 844 845 if not scored: 846 return responses[0] if responses else None 847 848 # Sort by score (descending) 849 scored.sort(key=lambda x: x[0], reverse=True) 850 return scored[0][1] 851 852 # ========== UTILITIES ========== 853 def display_results(result: QueryResult): 854 """Display results beautifully""" 855 856 print("\n" + "=" * 80) 857 print("š¤ AI FREEDOM ORCHESTRATOR v3.0") 858 print("=" * 80) 859 print(f"Query: {result.query}") 860 print(f"Processing time: {result.processing_time:.2f}s") 861 print(f"Total cost: ${result.total_cost:.6f}") 862 print(f"Successful responses: {len([r for r in result.responses if r.success])}") 863 print("=" * 80) 864 865 best_is_synthesis = False 866 if result.synthesis: 867 # Check if synthesis is truly unique or just a copy of the best response 868 if result.best_response and result.synthesis.strip() == result.best_response.content.strip(): 869 best_is_synthesis = False 870 else: 871 best_is_synthesis = True 872 873 if result.synthesis and best_is_synthesis: 874 print(f"\nš SYNTHESIZED BEST ANSWER:") 875 print("-" * 40) 876 print(result.synthesis) 877 print("-" * 40) 878 879 if result.best_response and not best_is_synthesis: 880 print(f"\nš BEST SINGLE RESPONSE:") 881 print("-" * 40) 882 print(f"Source: {result.best_response.source.value.upper()}") 883 print(f"Model: {result.best_response.model}") 884 print(f"Latency: {result.best_response.latency:.2f}s") 885 print(f"Cost: ${result.best_response.cost:.6f}") 886 print("-" * 40) 887 print(result.best_response.content) 888 print("-" * 40) 889 890 891 print(f"\nš ALL RESPONSES:") 892 print("-" * 80) 893 # Print header for table 894 print(f"{'Status'.ljust(8)} | {'Source'.ljust(12)} | {'Model'.ljust(20)} | {'Latency'.ljust(8)} | {'Cost'.ljust(12)} | {'Tokens'.ljust(15)} | {'Error/Content Snippet'}") 895 print("-" * 80) 896 897 for resp in result.responses: 898 status = "ā " if resp.success else "ā" 899 source = resp.source.value.ljust(12) 900 model = resp.model[:20].ljust(20) 901 latency = f"{resp.latency:.2f}s".ljust(8) 902 cost = f"${resp.cost:.6f}".ljust(12) 903 tokens = f"{resp.tokens_used} tokens".ljust(15) 904 905 if resp.success: 906 snippet = resp.content.strip().split('\n')[0][:30] + '...' 907 else: 908 snippet = resp.error[:30] if resp.error else 'No response/Unknown error' 909 910 print(f"{status.ljust(8)} | {source} | {model} | {latency} | {cost} | {tokens} | {snippet}") 911 912 print("=" * 80) 913 914 async def main(): 915 """Main entry point""" 916 917 print("\n" + "=" * 80) 918 print("š AI FREEDOM ORCHESTRATOR v3.0") 919 print("=" * 80) 920 print("Now with OpenAI and 7 other AI services!") 921 print("=" * 80) 922 923 print("\nAvailable strategies:") 924 print("1. balanced - Mix of free and paid (default)") 925 print("2. free_only - Only free APIs") 926 print("3. premium - Prefer OpenAI when available") 927 print("4. uncensored - Prefer uncensored models") 928 929 # Get query and strategy 930 if len(sys.argv) > 1: 931 # Use first argument as strategy if it matches, rest as query 932 possible_strategy = sys.argv[1].lower().strip() 933 if possible_strategy in ["balanced", "free_only", "premium", "uncensored"]: 934 strategy = possible_strategy 935 query = " ".join(sys.argv[2:]) 936 else: 937 strategy = "balanced" 938 query = " ".join(sys.argv[1:]) 939 940 if not query: 941 query = "Explain advanced AI concepts and their practical applications" 942 else: 943 print("\nEnter your query:") 944 query = input("> ").strip() 945 if not query: 946 query = "Explain advanced AI concepts and their practical applications" 947 948 print("\nSelect strategy (balanced/free_only/premium/uncensored):") 949 strategy = input("> ").strip().lower() 950 if strategy not in ["balanced", "free_only", "premium", "uncensored"]: 951 strategy = "balanced" 952 953 # Initialize orchestrator 954 async with APIManager() as api_manager: 955 orchestrator = AIFreedomOrchestrator(strategy=strategy) 956 orchestrator.api_manager = api_manager 957 958 try: 959 # Process query 960 print(f"\nProcessing with '{strategy}' strategy...") 961 result = await orchestrator.process_query(query, budget=0.10) 962 963 # Display results 964 display_results(result) 965 966 # Save results (FIXED: incomplete JSON serialization) 967 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 968 filename = f"ai_result_{timestamp}.json" 969 970 with open(filename, 'w', encoding='utf-8') as f: 971 # Use to_dict method from QueryResult for clean serialization 972 json.dump(result.to_dict(), f, indent=4, ensure_ascii=False) 973 974 print(f"\nResults saved to {filename}") 975 976 except KeyboardInterrupt: 977 print("\nOrchestrator stopped by user.") 978 except Exception as e: 979 logger.critical(f"A fatal error occurred in main: {e}") 980 981 if __name__ == "__main__": 982 try: 983 # The main entry point for an asyncio program 984 asyncio.run(main()) 985 except KeyboardInterrupt: 986 print("\nProgram exit.")