Spaces:
Paused
Paused
| import json | |
| import time | |
| import random | |
| from functools import wraps | |
| from typing import Any, Callable, Optional, Tuple, Type | |
| from .exceptions import ValidationError | |
| from .config import ( | |
| RETRY_MAX_ATTEMPTS, | |
| RETRY_BACKOFF_FACTOR, | |
| SEARCH_MODES, | |
| SEARCH_SOURCES, | |
| MODEL_MAPPINGS, | |
| RATE_LIMIT_MIN_DELAY, | |
| RATE_LIMIT_MAX_DELAY, | |
| ) | |
| from .logger import get_logger | |
| logger = get_logger("utils") | |
| def retry_with_backoff( | |
| max_attempts: int = RETRY_MAX_ATTEMPTS, | |
| backoff_factor: float = RETRY_BACKOFF_FACTOR, | |
| exceptions: Tuple[Type[Exception], ...] = (Exception,), | |
| on_retry: Optional[Callable[[int, Exception], None]] = None, | |
| ) -> Callable: | |
| def decorator(func: Callable) -> Callable: | |
| def wrapper(*args: Any, **kwargs: Any) -> Any: | |
| attempt = 0 | |
| while attempt < max_attempts: | |
| try: | |
| return func(*args, **kwargs) | |
| except exceptions as e: | |
| attempt += 1 | |
| if attempt >= max_attempts: | |
| logger.error(f"Failed after {max_attempts} attempts: {e}") | |
| raise | |
| wait_time = backoff_factor ** attempt + random.uniform(0, 1) | |
| logger.warning( | |
| f"Attempt {attempt}/{max_attempts} failed: {e}. " | |
| f"Retrying in {wait_time:.2f}s..." | |
| ) | |
| if on_retry: | |
| on_retry(attempt, e) | |
| time.sleep(wait_time) | |
| raise Exception(f"Failed after {max_attempts} attempts") | |
| return wrapper | |
| return decorator | |
| def rate_limit( | |
| min_delay: float = RATE_LIMIT_MIN_DELAY, | |
| max_delay: float = RATE_LIMIT_MAX_DELAY, | |
| ) -> Callable: | |
| def decorator(func: Callable) -> Callable: | |
| last_call = [0.0] | |
| def wrapper(*args: Any, **kwargs: Any) -> Any: | |
| delay = random.uniform(min_delay, max_delay) | |
| elapsed = time.time() - last_call[0] | |
| if elapsed < delay: | |
| sleep_time = delay - elapsed | |
| logger.debug(f"Rate limiting: waiting {sleep_time:.2f}s") | |
| time.sleep(sleep_time) | |
| last_call[0] = time.time() | |
| return func(*args, **kwargs) | |
| return wrapper | |
| return decorator | |
| def validate_search_params( | |
| mode: str, | |
| model: Optional[str], | |
| sources: list, | |
| own_account: bool = False, | |
| ) -> None: | |
| if mode not in SEARCH_MODES: | |
| raise ValidationError(f"Invalid mode '{mode}'. Must be one of: {', '.join(SEARCH_MODES)}") | |
| if model is not None: | |
| valid_models = list(MODEL_MAPPINGS.get(mode, {}).keys()) | |
| if model not in valid_models: | |
| raise ValidationError( | |
| f"Invalid model '{model}' for mode '{mode}'. " | |
| f"Valid models: {', '.join(str(m) for m in valid_models)}" | |
| ) | |
| if model is not None and not own_account: | |
| raise ValidationError( | |
| "Model selection requires an account with cookies. " | |
| "Initialize Client with cookies parameter." | |
| ) | |
| invalid_sources = [s for s in sources if s not in SEARCH_SOURCES] | |
| if invalid_sources: | |
| raise ValidationError( | |
| f"Invalid sources: {', '.join(invalid_sources)}. " | |
| f"Valid sources: {', '.join(SEARCH_SOURCES)}" | |
| ) | |
| if not sources: | |
| raise ValidationError("At least one source must be specified") | |
| def validate_query_limits( | |
| copilot_remaining: int, | |
| file_upload_remaining: int, | |
| mode: str, | |
| files_count: int, | |
| ) -> None: | |
| if mode in ["pro", "reasoning", "deep research"] and copilot_remaining <= 0: | |
| raise ValidationError( | |
| f"No remaining enhanced queries for mode '{mode}'. " | |
| f"Create a new account or use mode='auto'." | |
| ) | |
| if files_count > 0 and file_upload_remaining < files_count: | |
| raise ValidationError( | |
| f"Insufficient file uploads. Requested: {files_count}, " | |
| f"Available: {file_upload_remaining}" | |
| ) | |
| def sanitize_query(query: str) -> str: | |
| if not isinstance(query, str): | |
| raise ValidationError(f"Query must be string, got {type(query)}") | |
| query = query.strip() | |
| if not query: | |
| raise ValidationError("Query cannot be empty") | |
| if len(query) > 10000: | |
| raise ValidationError("Query is too long (max 10000 characters)") | |
| return query | |
| def parse_nested_json_response(content_json: dict) -> dict: | |
| if "text" in content_json and content_json["text"]: | |
| try: | |
| text_parsed = json.loads(content_json["text"]) | |
| if isinstance(text_parsed, list): | |
| for step in text_parsed: | |
| if step.get("step_type") == "FINAL": | |
| final_content = step.get("content", {}) | |
| if "answer" in final_content: | |
| try: | |
| answer_data = json.loads(final_content["answer"]) | |
| content_json["answer"] = answer_data.get("answer", "") | |
| content_json["chunks"] = answer_data.get("chunks", []) | |
| except (json.JSONDecodeError, TypeError): | |
| pass | |
| break | |
| content_json["text"] = text_parsed | |
| except (json.JSONDecodeError, TypeError, KeyError): | |
| pass | |
| return content_json | |