/ 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.")