diff --git "a/server.py" "b/server.py" deleted file mode 100644--- "a/server.py" +++ /dev/null @@ -1,2490 +0,0 @@ -# ============================================================================ -# 🧠 PINKSKY v6.0 — ПОЛНЫЙ ДОСТУП В ИНТЕРНЕТ ДЛЯ ВСЕХ РЕЖИМОВ -# ============================================================================ - -# === БАЗОВЫЕ ИМПОРТЫ === -import os -import json -import urllib.request -import sys -import time -import ssl -import requests -import threading -import socket -import re -import io -import signal -import weakref -import fnmatch -import hashlib -from http.server import BaseHTTPRequestHandler, HTTPServer -from typing import Dict, List, Optional, Any, Tuple, Callable -from dataclasses import dataclass, field -from datetime import datetime, timedelta -from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError -from collections import Counter -from functools import wraps - -# ============================================================================ -# 📦 ПРОВЕРКА ЗАВИСИМОСТЕЙ -# ============================================================================ - -try: - from interpreter import interpreter -except ImportError: - print("❌ open-interpreter не найден. Установи: pip install open-interpreter") - sys.exit(1) - -try: - from bs4 import BeautifulSoup -except ImportError: - print("⚠️ BeautifulSoup не найден. Установи: pip install beautifulsoup4") - # Создаём заглушку - class BeautifulSoup: - def __init__(self, *args, **kwargs): - pass - -# ============================================================================ -# 🔧 CUSTOM DNS RESOLVER -# ============================================================================ - -HF_DOMAIN = "api-inference.huggingface.co" -resolved_hf_ip = None - -def get_hf_ip_via_google(): - global resolved_hf_ip - if resolved_hf_ip: - return resolved_hf_ip - try: - req = urllib.request.Request(f"https://dns.google/resolve?name={HF_DOMAIN}&type=A") - with urllib.request.urlopen(req, timeout=5) as response: - data = json.loads(response.read().decode('utf-8')) - for answer in data.get("Answer", []): - if answer.get("type") == 1: - resolved_hf_ip = answer.get("data") - print(f"🔍 [DNS] HF IP через Google: {resolved_hf_ip}") - return resolved_hf_ip - except Exception as e: - print(f"⚠️ [DNS] Google DoH ошибка: {e}") - return None - -original_getaddrinfo = socket.getaddrinfo - -def custom_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): - if host == HF_DOMAIN: - ip = get_hf_ip_via_google() - if ip: - return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, '', (ip, port))] - return original_getaddrinfo(host, port, family, type, proto, flags) - -socket.getaddrinfo = custom_getaddrinfo - -# ============================================================================ -# 🔐 ENVIRONMENT CONFIG -# ============================================================================ - -PORT = 7860 -TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip() -ALLOWED_USER = os.environ.get("TELEGRAM_ALLOWED_USERS", "").strip() -CF_URL = os.environ.get("CF_WORKER_URL", "").rstrip('/') - -API_KEY = os.environ.get("NVIDIA_API_KEY", os.environ.get("OPENAI_API_KEY", "")).strip() -API_BASE = os.environ.get("NVIDIA_API_BASE", os.environ.get("OPENAI_API_BASE", "")).strip().rstrip('/') -if API_BASE and not API_BASE.endswith("/v1"): - API_BASE += "/v1" - -HF_TOKEN = os.environ.get("HF_TOKEN", "").strip() -HF_FALLBACK_MODEL = "Qwen/Qwen2.5-72B-Instruct" -# ============================================================================ -# 📁 FOLDERS -# ============================================================================ - -for folder in ["skills", "projects", "downloads", "prompts", "build_modes"]: - os.makedirs(folder, exist_ok=True) - -ROLES_FILE = "prompts/roles.json" -MODELS_FILE = "prompts/models.json" -CONDUCTORS_FILE = "prompts/conductors.json" -HISTORY_FILE = "prompts/history.json" -BUILD_MODES_FILE = "build_modes/build_modes.json" - -# ============================================================================ -# 🏗️ DATACLASSES -# ============================================================================ - -@dataclass -class ModelConfig: - name: str - provider: str - endpoint: str - api_key_env: str - context_window: int = 32000 - max_tokens: int = 8000 - cost_per_1k_input: float = 0.0 - cost_per_1k_output: float = 0.0 - coding_rank: int = 50 - speed_rank: int = 50 - reasoning_rank: int = 50 - tags: List[str] = field(default_factory=list) - -@dataclass -class Role: - name: str - prompt: str - description: str - preferred_models: List[str] = field(default_factory=list) - complexity: str = "medium" - tags: List[str] = field(default_factory=list) - tools: List[str] = field(default_factory=list) # 🔥 Список доступных инструментов - -@dataclass -class Conductor: - name: str - prompt: str - description: str - strategy: str = "parallel" - max_agents: int = 3 - cost_aware: bool = True - auto_rank_by: str = "coding" - -# ============================================================================ -# 🛑 PROCESS MANAGER -# ============================================================================ - -class ProcessManager: - def __init__(self): - self.active_threads: List[threading.Thread] = [] - self.cancel_flags: Dict[int, bool] = {} - self.lock = threading.Lock() - self.executor = ThreadPoolExecutor(max_workers=10) - self.futures = [] - - def register_thread(self, thread: threading.Thread) -> None: - with self.lock: - self.active_threads.append(thread) - self.cancel_flags[thread.ident] = False - - def register_future(self, future) -> None: - with self.lock: - self.futures.append(future) - - def cancel_all(self) -> str: - with self.lock: - for future in self.futures: - if not future.done(): - future.cancel() - self.futures.clear() - for thread_id in self.cancel_flags: - self.cancel_flags[thread_id] = True - for thread in self.active_threads: - if thread.is_alive(): - try: - thread.join(timeout=0.5) - except Exception: - pass - self.active_threads.clear() - self.cancel_flags.clear() - - try: - if hasattr(interpreter, 'cancel'): - interpreter.cancel() - except Exception: - pass - - STATE.cancel_flag = True - STATE.current_mode = "paused" - return "✅ Все процессы генерации остановлены!" - - def is_cancelled(self, thread_id: int = None) -> bool: - if thread_id is None: - thread_id = threading.current_thread().ident - with self.lock: - return self.cancel_flags.get(thread_id, False) - - def clear(self) -> None: - with self.lock: - self.active_threads = [t for t in self.active_threads if t.is_alive()] - - def get_active_count(self) -> int: - with self.lock: - return len([t for t in self.active_threads if t.is_alive()]) - -PROCESS_MANAGER = ProcessManager() -# ============================================================================ -# 🥇 AUTO-RANKING MODELS -# ============================================================================ - -MODEL_RANKING = { - "deepseek-v4-pro": { - "endpoint": "deepseek-ai/deepseek-v4-pro", - "context_window": 64000, "max_tokens": 8000, - "coding_rank": 1, "speed_rank": 15, "reasoning_rank": 1, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.005, - "tags": ["elite", "coding", "reasoning", "math", "cheap"] - }, - "kimi-k2.6": { - "endpoint": "moonshotai/kimi-k2.6", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 2, "speed_rank": 12, "reasoning_rank": 2, - "cost_per_1k_input": 0.003, "cost_per_1k_output": 0.015, - "tags": ["elite", "coding", "reasoning", "long_context"] - }, - "qwen3.5-397b": { - "endpoint": "qwen/qwen3.5-397b-a17b", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 3, "speed_rank": 18, "reasoning_rank": 3, - "cost_per_1k_input": 0.002, "cost_per_1k_output": 0.008, - "tags": ["elite", "coding", "long_context", "chinese"] - }, - "mistral-large-3": { - "endpoint": "mistralai/mistral-large-3-675b-instruct-2512", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 4, "speed_rank": 14, "reasoning_rank": 4, - "cost_per_1k_input": 0.002, "cost_per_1k_output": 0.010, - "tags": ["elite", "coding", "multilingual", "long_context"] - }, - "gpt-oss-120b": { - "endpoint": "openai/gpt-oss-120b", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 5, "speed_rank": 20, "reasoning_rank": 5, - "cost_per_1k_input": 0.003, "cost_per_1k_output": 0.012, - "tags": ["elite", "coding", "reasoning", "openai"] - }, - "deepseek-v4-flash": { - "endpoint": "deepseek-ai/deepseek-v4-flash", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 6, "speed_rank": 8, "reasoning_rank": 8, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["strong", "coding", "fast", "cheap"] - }, - "llama-4-maverick": { - "endpoint": "meta/llama-4-maverick-17b-128e-instruct", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 7, "speed_rank": 10, "reasoning_rank": 7, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004, - "tags": ["strong", "coding", "meta", "long_context"] - }, - "nemotron-3-super": { - "endpoint": "nvidia/nemotron-3-super-120b-a12b", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 8, "speed_rank": 16, "reasoning_rank": 6, - "cost_per_1k_input": 0.002, "cost_per_1k_output": 0.008, - "tags": ["strong", "coding", "nvidia", "reasoning"] - }, - "mistral-medium-3.5": { - "endpoint": "mistralai/mistral-medium-3.5-128b", - "context_window": 64000, "max_tokens": 8000, - "coding_rank": 9, "speed_rank": 11, "reasoning_rank": 10, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.005, - "tags": ["strong", "coding", "mistral", "balanced"] - }, - "dracarys-llama-70b": { - "endpoint": "abacusai/dracarys-llama-3.1-70b-instruct", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 10, "speed_rank": 13, "reasoning_rank": 11, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004, - "tags": ["strong", "coding", "roleplay", "creative"] - }, - "llama-3.3-70b": { - "endpoint": "meta/llama-3.3-70b-instruct", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 11, "speed_rank": 9, "reasoning_rank": 12, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["strong", "coding", "meta", "fast", "cheap"] - }, - "nemotron-super-49b": { - "endpoint": "nvidia/llama-3.3-nemotron-super-49b-v1.5", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 12, "speed_rank": 7, "reasoning_rank": 13, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["strong", "coding", "nvidia", "fast", "cheap"] - }, - "step-3.7-flash": { - "endpoint": "stepfun-ai/step-3.7-flash", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 13, "speed_rank": 6, "reasoning_rank": 14, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["good", "coding", "fast", "chinese", "cheap"] - }, - "mistral-small-4": { - "endpoint": "mistralai/mistral-small-4-119b-2603", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 14, "speed_rank": 5, "reasoning_rank": 15, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["good", "coding", "fast", "mistral", "cheap"] - }, - "minimax-m2.7": { - "endpoint": "minimaxai/minimax-m2.7", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 15, "speed_rank": 4, "reasoning_rank": 16, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["good", "coding", "fast", "chinese", "cheap"] - }, - "nemotron-super-49b-v1": { - "endpoint": "nvidia/llama-3.3-nemotron-super-49b-v1", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 16, "speed_rank": 17, "reasoning_rank": 17, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["good", "coding", "nvidia", "cheap"] - }, - "llama-3.2-90b-vision": { - "endpoint": "meta/llama-3.2-90b-vision-instruct", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 17, "speed_rank": 19, "reasoning_rank": 18, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004, - "tags": ["good", "coding", "vision", "multimodal", "meta"] - }, - "nemotron-nano-12b": { - "endpoint": "nvidia/nemotron-nano-12b-v2-vl", - "context_window": 16000, "max_tokens": 4000, - "coding_rank": 18, "speed_rank": 2, "reasoning_rank": 22, - "cost_per_1k_input": 0.0001, "cost_per_1k_output": 0.0005, - "tags": ["light", "fast", "vision", "nvidia", "cheap"] - }, - "nemotron-3-nano-30b": { - "endpoint": "nvidia/nemotron-3-nano-30b-a3b", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 19, "speed_rank": 3, "reasoning_rank": 19, - "cost_per_1k_input": 0.0002, "cost_per_1k_output": 0.001, - "tags": ["light", "fast", "nvidia", "cheap"] - }, - "nemotron-nano-9b": { - "endpoint": "nvidia/nvidia-nemotron-nano-9b-v2", - "context_window": 16000, "max_tokens": 4000, - "coding_rank": 20, "speed_rank": 1, "reasoning_rank": 23, - "cost_per_1k_input": 0.0001, "cost_per_1k_output": 0.0005, - "tags": ["light", "fastest", "nvidia", "cheap"] - }, - "nemotron-content-safety": { - "endpoint": "nvidia/nemotron-content-safety-reasoning-4b", - "context_window": 8000, "max_tokens": 2000, - "coding_rank": 21, "speed_rank": 1, "reasoning_rank": 24, - "cost_per_1k_input": 0.0001, "cost_per_1k_output": 0.0005, - "tags": ["light", "fastest", "safety", "nvidia", "cheap"] - }, - "nemotron-3-nano-omni": { - "endpoint": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 22, "speed_rank": 5, "reasoning_rank": 20, - "cost_per_1k_input": 0.0002, "cost_per_1k_output": 0.001, - "tags": ["specialized", "omni", "multimodal", "reasoning", "nvidia", "cheap"] - }, - "diffusiongemma": { - "endpoint": "google/diffusiongemma-26b-a4b-it", - "context_window": 16000, "max_tokens": 4000, - "coding_rank": 23, "speed_rank": 10, "reasoning_rank": 25, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004, - "tags": ["specialized", "image", "diffusion", "google"] - }, - "glm": { - "endpoint": "z-ai/glm-5.1", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 9, "speed_rank": 8, "reasoning_rank": 9, - "cost_per_1k_input": 0.002, "cost_per_1k_output": 0.008, - "tags": ["legacy", "coding", "fast", "chinese"] - }, - "hf_fallback": { - "endpoint": HF_FALLBACK_MODEL, - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 50, "speed_rank": 50, "reasoning_rank": 50, - "cost_per_1k_input": 0, "cost_per_1k_output": 0, - "tags": ["fallback", "free", "hf"] - }, -} -# ============================================================================ -# 📁 FILE MANAGER -# ============================================================================ - -class FileManager: - SUPPORTED_EXTENSIONS = { - '.txt': 'text', '.py': 'python', '.js': 'javascript', - '.html': 'html', '.css': 'css', '.json': 'json', - '.yaml': 'yaml', '.yml': 'yaml', '.md': 'markdown', - '.csv': 'csv', '.xml': 'xml', '.log': 'log', - '.sql': 'sql', '.sh': 'bash', '.bat': 'batch', - '.ps1': 'powershell', '.ipynb': 'jupyter', - } - - def __init__(self, base_dir: str = "."): - self.base_dir = base_dir - - def read_file(self, file_path: str, max_size: int = 100000) -> Tuple[str, str]: - full_path = os.path.join(self.base_dir, file_path) - if not os.path.exists(full_path): - return "", "not_found" - ext = os.path.splitext(file_path)[1].lower() - file_type = self.SUPPORTED_EXTENSIONS.get(ext, 'binary') - size = os.path.getsize(full_path) - if size > max_size: - return f"⚠️ Файл слишком большой ({size} байт). Показано {max_size} байт.", file_type - try: - with open(full_path, 'r', encoding='utf-8') as f: - content = f.read(max_size) - return content, file_type - except UnicodeDecodeError: - with open(full_path, 'rb') as f: - content = f.read(max_size) - return f"📎 Бинарный файл ({size} байт)", 'binary' - except Exception as e: - return f"❌ Ошибка чтения: {e}", 'error' - - def read_multiple_files(self, file_paths: List[str], max_total: int = 50000) -> Dict[str, Tuple[str, str]]: - results = {} - total_size = 0 - for path in file_paths: - content, file_type = self.read_file(path) - if total_size + len(content) > max_total: - results[path] = (f"⚠️ Превышен лимит ({max_total} байт)", file_type) - else: - results[path] = (content, file_type) - total_size += len(content) - return results - - def list_files(self, directory: str = ".", pattern: str = "*", recursive: bool = False) -> List[str]: - full_path = os.path.join(self.base_dir, directory) - if not os.path.exists(full_path): - return [] - files = [] - if recursive: - for root, _, filenames in os.walk(full_path): - for filename in filenames: - if self._match_pattern(filename, pattern): - rel_path = os.path.relpath(os.path.join(root, filename), self.base_dir) - files.append(rel_path) - else: - for filename in os.listdir(full_path): - if os.path.isfile(os.path.join(full_path, filename)): - if self._match_pattern(filename, pattern): - files.append(os.path.join(directory, filename)) - return files - - def save_file(self, file_path: str, content: str) -> str: - full_path = os.path.join(self.base_dir, file_path) - os.makedirs(os.path.dirname(full_path), exist_ok=True) - try: - with open(full_path, 'w', encoding='utf-8') as f: - f.write(content) - return f"✅ Файл сохранён: {file_path}" - except Exception as e: - return f"❌ Ошибка сохранения: {e}" - - def analyze_file(self, file_path: str) -> Dict[str, Any]: - full_path = os.path.join(self.base_dir, file_path) - if not os.path.exists(full_path): - return {"error": "Файл не найден"} - stat = os.stat(full_path) - ext = os.path.splitext(file_path)[1].lower() - result = { - "name": os.path.basename(file_path), - "path": file_path, - "size_bytes": stat.st_size, - "type": self.SUPPORTED_EXTENSIONS.get(ext, 'unknown'), - "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(), - "created": datetime.fromtimestamp(stat.st_ctime).isoformat(), - } - if result["type"] != 'binary' and result["type"] != 'unknown': - content, _ = self.read_file(file_path, max_size=100000) - if content: - lines = content.split('\n') - result["lines"] = len(lines) - result["chars"] = len(content) - result["words"] = len(content.split()) - return result - - def _match_pattern(self, filename: str, pattern: str) -> bool: - if pattern == "*": - return True - return fnmatch.fnmatch(filename, pattern) - -FILE_MANAGER = FileManager() -# ============================================================================ -# 🌐 ПОЛНОСТЬЮ БЕСПЛАТНЫЙ ИНТЕРНЕТ-АГЕНТ -# ============================================================================ - -class FreeInternetAgent: - """Интернет-агент с бесплатными поисковыми системами""" - - def __init__(self, cache_ttl: int = 3600): - self.session = requests.Session() - self.session.headers.update({ - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - }) - self.cache: Dict[str, Tuple[Any, datetime]] = {} - self.cache_ttl = cache_ttl - self._has_bs4 = 'BeautifulSoup' in globals() - - # SearXNG инстансы (можно добавить свои) - self.searxng_instances = [ - "https://searx.be", - "https://search.bus-hit.me", - "https://searx.nixnet.xyz", - "https://searx.tuxcloud.net", - "https://searx.moe", - ] - - def search_web(self, query: str, num_results: int = 5) -> List[Dict[str, str]]: - """Умный поиск с несколькими источниками""" - - cache_key = self._get_cache_key('search', query, num_results) - cached = self._get_from_cache(cache_key) - if cached is not None: - return cached - - results = [] - - # 🥇 SearXNG - results = self._search_searxng(query, num_results) - - # 🥈 DuckDuckGo - if not results: - results = self._search_duckduckgo(query, num_results) - - # 🥉 Google - if not results: - results = self._search_google(query, num_results) - - # 🏁 Яндекс (для русского) - if not results and any(ord(c) > 1024 for c in query): - results = self._search_yandex(query, num_results) - - self._save_to_cache(cache_key, results) - return results - - def _search_searxng(self, query: str, num_results: int) -> List[Dict[str, str]]: - """Поиск через SearXNG (мета-поиск)""" - results = [] - - for instance in self.searxng_instances: - try: - url = f"{instance}/search" - params = { - "q": query, - "format": "json", - "categories": "general", - "engines": "google,bing,duckduckgo,startpage", - "language": "en", - "pageno": 1 - } - - response = self.session.get(url, params=params, timeout=20) - response.raise_for_status() - data = response.json() - - if 'results' in data: - for item in data['results'][:num_results]: - results.append({ - 'title': item.get('title', '')[:100], - 'url': item.get('url', ''), - 'snippet': item.get('content', '')[:200], - 'source': 'searxng', - 'engine': item.get('engine', '') - }) - - if results: - print(f"🔍 SearXNG: {len(results)} результатов") - break - - except Exception as e: - continue - - return results - - def _search_duckduckgo(self, query: str, num_results: int) -> List[Dict[str, str]]: - """Поиск через DuckDuckGo API""" - results = [] - try: - url = f"https://api.duckduckgo.com/?q={query}&format=json&no_html=1&skip_disambig=1" - response = self.session.get(url, timeout=15) - data = response.json() - - if 'RelatedTopics' in data: - for item in data['RelatedTopics'][:num_results]: - if 'Text' in item and 'FirstURL' in item: - results.append({ - 'title': item['Text'][:100], - 'url': item['FirstURL'], - 'snippet': item.get('Text', '')[:200], - 'source': 'duckduckgo' - }) - - print(f"🦆 DuckDuckGo: {len(results)} результатов") - - except Exception as e: - print(f"⚠️ DuckDuckGo ошибка: {e}") - - return results - - def _search_google(self, query: str, num_results: int) -> List[Dict[str, str]]: - """Поиск через Google (парсинг)""" - results = [] - if not self._has_bs4: - return results - - try: - from bs4 import BeautifulSoup - - url = f"https://www.google.com/search?q={query}&num={num_results * 2}" - response = self.session.get(url, timeout=20) - soup = BeautifulSoup(response.text, 'html.parser') - - for g in soup.find_all('div', class_='g'): - title_elem = g.find('h3') - link_elem = g.find('a') - snippet_elem = g.find('div', class_='VwiC3b') - - if title_elem and link_elem: - title = title_elem.get_text() - link = link_elem.get('href', '') - snippet = snippet_elem.get_text() if snippet_elem else '' - - if link.startswith('/url?q='): - link = link.split('/url?q=')[1].split('&')[0] - - if link.startswith('http'): - results.append({ - 'title': title[:100], - 'url': link, - 'snippet': snippet[:200], - 'source': 'google' - }) - - if len(results) >= num_results: - break - - print(f"🔍 Google: {len(results)} результатов") - - except Exception as e: - print(f"⚠️ Google ошибка: {e}") - - return results - - def _search_yandex(self, query: str, num_results: int) -> List[Dict[str, str]]: - """Поиск через Яндекс (парсинг)""" - results = [] - if not self._has_bs4: - return results - - try: - from bs4 import BeautifulSoup - - url = f"https://yandex.ru/search/?text={query}&numdoc={num_results}" - response = self.session.get(url, timeout=20) - soup = BeautifulSoup(response.text, 'html.parser') - - for item in soup.find_all('li', class_='serp-item'): - link_elem = item.find('a', class_='link') - snippet_elem = item.find('div', class_='text-container') - - if link_elem: - title = link_elem.get_text() - link = link_elem.get('href', '') - snippet = snippet_elem.get_text() if snippet_elem else '' - - if link.startswith('http'): - results.append({ - 'title': title[:100], - 'url': link, - 'snippet': snippet[:200], - 'source': 'yandex' - }) - - if len(results) >= num_results: - break - - print(f"🔍 Яндекс: {len(results)} результатов") - - except Exception as e: - print(f"⚠️ Яндекс ошибка: {e}") - - return results - - # ... остальные методы (fetch_page, analyze_website и т.д.) такие же ... - - # Извлекаем ссылки и изображения (с BeautifulSoup если доступен) - if self._has_bs4 and 'content' in page: - soup = BeautifulSoup(content, 'html.parser') - links = [a.get('href') for a in soup.find_all('a') if a.get('href') and a.get('href').startswith('http')] - images = [img.get('src') for img in soup.find_all('img') if img.get('src')] - else: - links = re.findall(r'href=["\'](https?://[^"\']+)["\']', content, re.IGNORECASE)[:20] - images = re.findall(r'src=["\'](https?://[^"\']+\.(jpg|jpeg|png|gif|svg))["\']', content, re.IGNORECASE) - images = [img[0] if isinstance(img, tuple) else img for img in images] - - result = { - 'url': url, - 'title': page.get('title', ''), - 'content_length': len(content), - 'word_count': len(content.split()), - 'links': links[:20], - 'images': images[:10], - 'keywords': self._extract_keywords(content), - 'language': self._detect_language(content), - 'has_form': ' List[Dict[str, Any]]: - """Загружает несколько страниц (доступен для агентов)""" - results = [] - total_size = 0 - for url in urls[:10]: - if total_size > max_total: - results.append({'url': url, 'error': 'Превышен лимит', 'content': ''}) - continue - result = self.fetch_page(url) - results.append(result) - total_size += len(result.get('content', '')) - return results - - # ================================================================ - # ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ - # ================================================================ - - def _extract_keywords(self, content: str) -> List[str]: - """Извлекает ключевые слова""" - text = re.sub(r'<[^>]+>', ' ', content) - words = re.findall(r'\b[a-zA-Z]{3,}\b', text) - counter = Counter(words) - return [word for word, _ in counter.most_common(10)] - - def _detect_language(self, content: str) -> str: - """Определяет язык страницы""" - text = re.sub(r'<[^>]+>', ' ', content) - lang_indicators = { - 'ru': ['и', 'в', 'не', 'на', 'я', 'что', 'с', 'а', 'по', 'это'], - 'en': ['the', 'and', 'to', 'of', 'for', 'with', 'on', 'at', 'from', 'by'], - 'de': ['der', 'die', 'und', 'den', 'mit', 'auf', 'für', 'von', 'im', 'nicht'], - } - words = set(text.lower().split()) - best_lang = 'en' - best_score = 0 - for lang, indicators in lang_indicators.items(): - score = sum(1 for word in indicators if word in words) - if score > best_score: - best_score = score - best_lang = lang - return best_lang - - def clear_cache(self) -> str: - """Очищает кэш""" - count = len(self.cache) - self.cache.clear() - return f"🗑️ Кэш очищен ({count} записей)" - - def get_cache_stats(self) -> str: - """Статистика кэша""" - return f"📊 Кэш: {len(self.cache)} записей, TTL: {self.cache_ttl} секунд" - -# Создаём глобальный экземпляр -INTERNET_AGENT = FreeInternetAgent(cache_ttl=3600) -# ============================================================================ -# 🔔 NOTIFICATION SYSTEM -# ============================================================================ - -class NotificationSystem: - CRITICAL_ACTIONS = [ - 'delete', 'remove', 'kill', 'stop', 'shutdown', - 'format', 'clear', 'reset', 'purge', - 'upload', 'publish', 'deploy', 'push', - 'change_password', 'add_user', 'remove_user', - 'grant_access', 'revoke_access', - 'install', 'uninstall', 'update', - 'execute', 'run', 'start', 'stop', - 'create_file', 'delete_file', 'modify_file', - ] - - HIGH_RISK_PATTERNS = [ - r'rm\s+-rf', r'del\s+/f', r'format\s+', r'mkfs', - r'drop\s+database', r'truncate\s+', r'delete\s+from', - r'ALTER\s+TABLE', r'DROP\s+TABLE', - r'chmod\s+777', r'chown\s+root', - r'sudo\s+', r'admin\s+', - ] - - def __init__(self, chat_id: str = None): - self.chat_id = chat_id - self.notification_history = [] - self.enabled = True - - def set_chat_id(self, chat_id: str): - self.chat_id = chat_id - - def set_enabled(self, enabled: bool): - self.enabled = enabled - - def check_action(self, action: str, context: Dict[str, Any] = None) -> bool: - action_lower = action.lower() - context = context or {} - for critical in self.CRITICAL_ACTIONS: - if critical in action_lower: - return True - for pattern in self.HIGH_RISK_PATTERNS: - if re.search(pattern, action_lower, re.IGNORECASE): - return True - if context.get('important', False): - return True - if context.get('files_changed', 0) > 3: - return True - return False - - def notify(self, action: str, details: str = "", severity: str = "info") -> str: - if not self.enabled: - return "🔇 Уведомления отключены" - if not self.chat_id: - return "⚠️ Chat ID не установлен" - emoji_map = {'critical': '🚨', 'warning': '���️', 'info': 'ℹ️', 'success': '✅', 'error': '❌'} - emoji = emoji_map.get(severity, 'ℹ️') - message = f"{emoji} *УВЕДОМЛЕНИЕ*\n\n🔹 *Действие:* `{action}`\n" - if details: - message += f"📝 *Детали:*\n```\n{details[:500]}\n```\n" - message += f"🕐 *Время:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - self.notification_history.append({ - 'action': action, 'details': details, - 'severity': severity, 'timestamp': datetime.now().isoformat() - }) - send_tg(self.chat_id, message) - return f"✅ Уведомление отправлено: {action}" - - def get_history(self, limit: int = 10) -> str: - if not self.notification_history: - return "📋 Нет уведомлений" - result = "📋 *История уведомлений:*\n\n" - for entry in self.notification_history[-limit:]: - emoji = {'critical': '🚨', 'warning': '⚠️', 'info': 'ℹ️'}.get(entry['severity'], 'ℹ️') - result += f"{emoji} `{entry['action']}` — {entry['timestamp']}\n" - return result - -NOTIFICATIONS = NotificationSystem() -# ============================================================================ -# 🌍 GLOBAL STATE -# ============================================================================ - -class PinkSkyState: - def __init__(self): - self.models: Dict[str, ModelConfig] = {} - self.roles: Dict[str, Role] = {} - self.conductors: Dict[str, Conductor] = {} - self.current_mode: str = "chat" - self.current_conductor: str = "default" - self.current_role: str = "universal" - self.current_model: str = "deepseek-v4-pro" - self.chat_history: List[Dict[str, str]] = [] - self.skill_history: List[Dict[str, str]] = [] - self.build_history: List[Dict[str, str]] = [] - self.build_context: Dict[str, Any] = { - "spec": "", "agents": 3, "models_tier": "tier1", - "skills_count": 2, "files_count": 3, "role": "universal", - "strategy": "parallel", "use_interpreter": True, - "notifications": True, "internet_access": True # 🔥 Включаем интернет - } - self.cancel_flag: bool = False - self.load_all() - - def load_all(self): - self._load_models() - self._load_roles() - self._load_conductors() - self._load_history() - - def _build_model_config(self, name: str, data: dict) -> ModelConfig: - return ModelConfig( - name=name, provider="openai", endpoint=data["endpoint"], - api_key_env="NVIDIA_API_KEY", - context_window=data.get("context_window", 32000), - max_tokens=data.get("max_tokens", 8000), - cost_per_1k_input=data.get("cost_per_1k_input", 0.0), - cost_per_1k_output=data.get("cost_per_1k_output", 0.0), - coding_rank=data.get("coding_rank", 50), - speed_rank=data.get("speed_rank", 50), - reasoning_rank=data.get("reasoning_rank", 50), - tags=data.get("tags", []) - ) - - def _load_models(self): - defaults = {name: self._build_model_config(name, data) for name, data in MODEL_RANKING.items()} - if os.path.exists(MODELS_FILE): - try: - with open(MODELS_FILE, "r", encoding="utf-8") as f: - custom = json.load(f) - for k, v in custom.items(): - if k not in defaults: - defaults[k] = ModelConfig(**v) - except Exception as e: - print(f"⚠️ Ошибка загрузки models.json: {e}") - self.models = defaults - - def _load_roles(self): - defaults = { - "universal": Role( - name="universal", - prompt="Ты PinkSky -- универсальный ИИ-ассистент. Можешь использовать интернет для поиска информации.", - description="Универсальный ассистент", - preferred_models=["deepseek-v4-pro", "kimi-k2.6"], - complexity="medium", - tags=["general"], - tools=["search_web", "fetch_page", "analyze_website"] # 🔥 Инструменты - ), - "guru": Role( - name="guru", - prompt="Ты Guru Programmer PinkSky. 15+ лет опыта. Можешь искать в интернете лучшие практики и примеры кода.", - description="Гуру-программист", - preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3"], - complexity="high", - tags=["coding", "senior"], - tools=["search_web", "fetch_page", "analyze_website"] - ), - "hacker": Role( - name="hacker", - prompt="Ты Hacker PinkSky. Виртуоз кода. Ищешь нестандартные решения, в том числе через интернет.", - description="Хакер-кодер", - preferred_models=["deepseek-v4-pro", "deepseek-v4-flash", "llama-4-maverick"], - complexity="high", - tags=["coding", "optimization"], - tools=["search_web", "fetch_page", "analyze_website"] - ), - "architect": Role( - name="architect", - prompt="Ты Software Architect PinkSky. Проектируешь системы. Изучаешь лучшие практики в интернете.", - description="Архитектор ПО", - preferred_models=["deepseek-v4-pro", "kimi-k2.6", "nemotron-3-super"], - complexity="high", - tags=["architecture", "design"], - tools=["search_web", "fetch_page", "analyze_website"] - ), - "researcher": Role( - name="researcher", - prompt="Ты Researcher PinkSky. Глубокий анализ тем. Активно используешь интернет для сбора информации.", - description="Исследователь", - preferred_models=["deepseek-v4-pro", "qwen3.5-397b"], - complexity="high", - tags=["research", "analysis"], - tools=["search_web", "fetch_page", "analyze_website"] - ), - "critic": Role( - name="critic", - prompt="Ты Critic and Auditor PinkSky. Проверяешь безопасность. Ищешь уязвимости в интернете.", - description="Критик и аудитор", - preferred_models=["deepseek-v4-pro", "kimi-k2.6"], - complexity="medium", - tags=["audit", "security"], - tools=["search_web", "fetch_page", "analyze_website"] - ), - } - # Добавляем остальные роли без инструментов - if os.path.exists(ROLES_FILE): - try: - with open(ROLES_FILE, "r", encoding="utf-8") as f: - custom = json.load(f) - for k, v in custom.items(): - if k not in defaults: - defaults[k] = Role(**v) - except Exception as e: - print(f"⚠️ Ошибка загрузки roles.json: {e}") - self.roles = defaults - - def _load_conductors(self): - defaults = { - "default": Conductor(name="default", prompt="Ты Conductor PinkSky (Default).", description="Стандартный кондуктор", strategy="selective", max_agents=3, cost_aware=True, auto_rank_by="balanced"), - "strict": Conductor(name="strict", prompt="Ты Strict Conductor PinkSky.", description="Минимум агентов", strategy="single", max_agents=1, cost_aware=True, auto_rank_by="coding"), - "creative": Conductor(name="creative", prompt="Ты Creative Conductor PinkSky.", description="Мозговой штурм", strategy="parallel", max_agents=5, cost_aware=False, auto_rank_by="coding"), - "economy": Conductor(name="economy", prompt="Ты Economy Conductor PinkSky.", description="Экономия бюджета", strategy="single", max_agents=1, cost_aware=True, auto_rank_by="speed"), - "review": Conductor(name="review", prompt="Ты Code Review Conductor PinkSky.", description="Code review", strategy="parallel", max_agents=4, cost_aware=True, auto_rank_by="coding"), - "build": Conductor(name="build", prompt="Ты Project Build Conductor PinkSky.", description="Билд проекта", strategy="sequential", max_agents=5, cost_aware=True, auto_rank_by="coding"), - } - if os.path.exists(CONDUCTORS_FILE): - try: - with open(CONDUCTORS_FILE, "r", encoding="utf-8") as f: - custom = json.load(f) - for k, v in custom.items(): - defaults[k] = Conductor(**v) - except Exception as e: - print(f"⚠️ Ошибка загрузки conductors.json: {e}") - self.conductors = defaults - - def _load_history(self): - if os.path.exists(HISTORY_FILE): - try: - with open(HISTORY_FILE, "r", encoding="utf-8") as f: - data = json.load(f) - self.chat_history = data.get("chat", []) - self.skill_history = data.get("skill", []) - self.build_history = data.get("build", []) - except Exception as e: - print(f"⚠️ Ошибка загрузки истории: {e}") - - def save_history(self): - data = {"chat": self.chat_history, "skill": self.skill_history, "build": self.build_history} - with open(HISTORY_FILE, "w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=2) - - def get_best_model(self, rank_by: str = "coding", min_tier: int = 1, max_tier: int = 5, exclude: List[str] = None) -> str: - exclude = exclude or [] - candidates = [] - for name, model in self.models.items(): - if name in exclude or name == "hf_fallback": - continue - tier = 5 - if model.coding_rank <= 5: tier = 1 - elif model.coding_rank <= 12: tier = 2 - elif model.coding_rank <= 18: tier = 3 - elif model.coding_rank <= 24: tier = 4 - if min_tier <= tier <= max_tier: - candidates.append((name, model)) - if not candidates: - return "deepseek-v4-pro" - if rank_by == "coding": - candidates.sort(key=lambda x: x[1].coding_rank) - elif rank_by == "speed": - candidates.sort(key=lambda x: x[1].speed_rank) - elif rank_by == "reasoning": - candidates.sort(key=lambda x: x[1].reasoning_rank) - elif rank_by == "balanced": - candidates.sort(key=lambda x: (x[1].coding_rank + x[1].speed_rank + x[1].reasoning_rank) / 3) - else: - candidates.sort(key=lambda x: x[1].coding_rank) - return candidates[0][0] - - def get_model_for_role(self, role_name: str, preference: str = None, rank_by: str = None) -> str: - role = self.roles.get(role_name) - if not role: - return preference or self.current_model - conductor = self.conductors.get(self.current_conductor, self.conductors["default"]) - rank_criteria = rank_by or conductor.auto_rank_by - max_tier = 2 if role.complexity == "high" else 3 if role.complexity == "medium" else 5 - if preference and preference in self.models: - return preference - available = [m for m in role.preferred_models if m in self.models and m != "hf_fallback"] - if available: - if conductor.cost_aware and rank_criteria != "coding": - available.sort(key=lambda m: self.models[m].cost_per_1k_output) - else: - if rank_criteria == "coding": - available.sort(key=lambda m: self.models[m].coding_rank) - elif rank_criteria == "speed": - available.sort(key=lambda m: self.models[m].speed_rank) - elif rank_criteria == "reasoning": - available.sort(key=lambda m: self.models[m].reasoning_rank) - else: - available.sort(key=lambda m: (self.models[m].coding_rank + self.models[m].speed_rank + self.models[m].reasoning_rank) / 3) - return available[0] - return self.get_best_model(rank_by=rank_criteria, max_tier=max_tier) - - def get_models_by_tier(self, tier: int) -> List[str]: - result = [] - for name, model in self.models.items(): - if name == "hf_fallback": - continue - model_tier = 5 - if model.coding_rank <= 5: model_tier = 1 - elif model.coding_rank <= 12: model_tier = 2 - elif model.coding_rank <= 18: model_tier = 3 - elif model.coding_rank <= 24: model_tier = 4 - if model_tier == tier: - result.append((name, model.coding_rank)) - result.sort(key=lambda x: x[1]) - return [name for name, _ in result] - - def get_next_tier_model(self, current_model_name: str) -> Optional[str]: - if current_model_name not in self.models: - return None - current = self.models[current_model_name] - current_tier = 5 - if current.coding_rank <= 5: current_tier = 1 - elif current.coding_rank <= 12: current_tier = 2 - elif current.coding_rank <= 18: current_tier = 3 - elif current.coding_rank <= 24: current_tier = 4 - for next_tier in range(current_tier + 1, 6): - models = self.get_models_by_tier(next_tier) - if models: - return models[0] - return None - - def add_to_history(self, mode: str, role: str, content: str): - entry = {"role": role, "content": content, "timestamp": datetime.now().isoformat()} - if mode == "chat": - self.chat_history.append(entry) - elif mode == "skill": - self.skill_history.append(entry) - elif mode == "build": - self.build_history.append(entry) - self.save_history() - - def export_history_json(self) -> str: - return json.dumps({"exported_at": datetime.now().isoformat(), "chat": self.chat_history, "skill": self.skill_history, "build": self.build_history}, ensure_ascii=False, indent=2) - - def export_history_md(self) -> str: - lines = [f"# 📜 История PinkSky\n\n*Экспорт: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n"] - lines.append("## 💬 Chat Mode\n") - for entry in self.chat_history: - emoji = "👤" if entry["role"] == "user" else "🤖" - lines.append(f"**{emoji} {entry['role'].upper()}** ({entry.get('timestamp', '')}):\n```\n{entry['content'][:500]}{'...' if len(entry['content']) > 500 else ''}\n```\n") - lines.append("## 🔧 Skill Mode\n") - for entry in self.skill_history: - emoji = "👤" if entry["role"] == "user" else "🤖" - lines.append(f"**{emoji} {entry['role'].upper()}** ({entry.get('timestamp', '')}):\n```\n{entry['content'][:500]}{'...' if len(entry['content']) > 500 else ''}\n```\n") - lines.append("## 🏗️ Build Mode\n") - for entry in self.build_history: - emoji = "👤" if entry["role"] == "user" else "🤖" - lines.append(f"**{emoji} {entry['role'].upper()}** ({entry.get('timestamp', '')}):\n```\n{entry['content'][:500]}{'...' if len(entry['content']) > 500 else ''}\n```\n") - return "\n".join(lines) - -STATE = PinkSkyState() -# ============================================================================ -# 🏗️ BUILD MODE EDITOR -# ============================================================================ - -class BuildModeEditor: - def __init__(self, state: PinkSkyState): - self.state = state - - def list_modes(self) -> str: - modes = self._load_all_modes() - if not modes: - return "📋 *Нет сохранённых режимов сборки*" - result = "📋 *Доступные режимы сборки:*\n" - for name, config in modes.items(): - desc = config.get("description", config.get("strategy", "Без описания")) - agents = config.get("agents", "?") - tier = config.get("models_tier", "?") - result += f"\n• `{name}` — {desc} (агенты: {agents}, тир: {tier})" - result += f"\n\n🔄 *Текущий контекст:*\n" - result += f"• Агенты: {self.state.build_context['agents']}\n" - result += f"• Тир: {self.state.build_context['models_tier']}\n" - result += f"• Интернет: {'✅' if self.state.build_context.get('internet_access', True) else '❌'}" - result += f"\n• OI: {'✅' if self.state.build_context['use_interpreter'] else '❌'}" - return result - - def show_mode(self, mode_name: str) -> str: - modes = self._load_all_modes() - config = modes.get(mode_name) - if not config: - return f"❌ Режим `{mode_name}` не найден" - result = f"📋 *Режим сборки: {mode_name}*\n\n```json\n{json.dumps(config, indent=2, ensure_ascii=False)}\n```" - return result - - def save_mode(self, mode_name: str, config: Dict = None) -> str: - if config is None: - config = { - "description": f"Режим {mode_name}", - "agents": self.state.build_context["agents"], - "models_tier": self.state.build_context["models_tier"], - "roles": [self.state.current_role], - "strategy": self.state.build_context["strategy"], - "skills_count": self.state.build_context["skills_count"], - "files_count": self.state.build_context["files_count"], - "use_interpreter": self.state.build_context.get("use_interpreter", True), - "internet_access": self.state.build_context.get("internet_access", True), - "conductor": self.state.current_conductor - } - modes = self._load_all_modes() - modes[mode_name] = config - try: - with open(BUILD_MODES_FILE, "w", encoding="utf-8") as f: - json.dump(modes, f, ensure_ascii=False, indent=2) - return f"✅ Режим `{mode_name}` сохранён!" - except Exception as e: - return f"❌ Ошибка сохранения: {e}" - - def load_mode(self, mode_name: str) -> str: - modes = self._load_all_modes() - config = modes.get(mode_name) - if not config: - return f"❌ Режим `{mode_name}` не найден" - self.state.build_context.update({ - "agents": config.get("agents", 3), - "models_tier": config.get("models_tier", "tier1"), - "skills_count": config.get("skills_count", 2), - "files_count": config.get("files_count", 3), - "strategy": config.get("strategy", "parallel"), - "use_interpreter": config.get("use_interpreter", True), - "internet_access": config.get("internet_access", True), - "role": config.get("roles", ["universal"])[0] if config.get("roles") else "universal" - }) - if config.get("conductor"): - self.state.current_conductor = config["conductor"] - if config.get("roles"): - self.state.current_role = config["roles"][0] - self.state.build_context["current_mode_name"] = mode_name - result = f"✅ Режим `{mode_name}` загружен!\n\n" - result += f"• Агенты: {config.get('agents')}\n" - result += f"• Тир: {config.get('models_tier')}\n" - result += f"• Интернет: {'✅' if config.get('internet_access', True) else '❌'}" - result += f"\n• OI: {'✅' if config.get('use_interpreter', True) else '❌'}" - return result - - def delete_mode(self, mode_name: str) -> str: - modes = self._load_all_modes() - if mode_name not in modes: - return f"❌ Режим `{mode_name}` не найден" - del modes[mode_name] - try: - with open(BUILD_MODES_FILE, "w", encoding="utf-8") as f: - json.dump(modes, f, ensure_ascii=False, indent=2) - return f"🗑️ Режим `{mode_name}` удалён!" - except Exception as e: - return f"❌ Ошибка удаления: {e}" - - def _load_all_modes(self) -> Dict: - if not os.path.exists(BUILD_MODES_FILE): - return {} - try: - with open(BUILD_MODES_FILE, "r", encoding="utf-8") as f: - return json.load(f) - except: - return {} - -# ============================================================================ -# 🤖 UNIVERSAL AGENT — С ПОЛНЫМ ДОСТУПОМ К ИНТЕРНЕТУ (5.1) -# ============================================================================ - -class UniversalAgent: - def __init__(self, role: Role, model: ModelConfig, use_interpreter: bool = False): - self.role = role - self.model = model - self.use_interpreter = use_interpreter - self.conversation_history: List[Dict[str, str]] = [] - self.file_manager = FILE_MANAGER - self.internet = INTERNET_AGENT - self.notifications = NOTIFICATIONS - - # 5.1: Регистрируем доступные инструменты - self.tools = { - "search_web": self.internet.search_web, - "fetch_page": self.internet.fetch_page, - "analyze_website": self.internet.analyze_website, - "read_file": self.file_manager.read_file, - "save_file": self.file_manager.save_file, - "list_files": self.file_manager.list_files, - "analyze_file": self.file_manager.analyze_file, - } - - def _is_valid_response(self, text: str) -> bool: - if not text or len(text.strip()) < 10: - return False - error_patterns = [r"^API Error:", r"^HF API Error:", r"^Both systems failed", r"^Error:", r"^Task \d+ error:", r"^Timeout$", r"^Conductor could not create a plan\."] - for pattern in error_patterns: - if re.search(pattern, text, re.IGNORECASE): - return False - return True - - def execute(self, task: str, sys_prompt_override: str = None, chat_id: str = None, history: List[Dict[str, str]] = None, mode: str = "chat") -> str: - if PROCESS_MANAGER.is_cancelled(): - return "🛑 Процесс отменён пользователем." - - if chat_id: - self.notifications.set_chat_id(chat_id) - if STATE.build_context.get("notifications", True) and self.notifications.check_action(task): - self.notifications.notify("Агент выполняет действие", f"{task[:200]}", severity="warning") - - # 🔥 Добавляем описание инструментов к задаче - enhanced_task = self._add_tools_to_task(task) - - if self.use_interpreter: - return self._execute_with_interpreter(enhanced_task, chat_id, mode) - else: - return self._execute_with_api(enhanced_task, sys_prompt_override, chat_id, history, mode) - - def _add_tools_to_task(self, task: str) -> str: - """Добавляет описание доступных инструментов""" - - # Определяем, какие инструменты доступны для этой роли - role_tools = getattr(self.role, 'tools', []) - if not role_tools: - # Если не указано, даём все основные - role_tools = ["search_web", "fetch_page", "analyze_website", "read_file", "save_file"] - - # Проверяем, включён ли интернет в контексте - internet_enabled = STATE.build_context.get("internet_access", True) - - tools_desc = """ -📁 ДОСТУПНЫЕ ИНСТРУМЕНТЫ: -""" - - if internet_enabled: - tools_desc += """ -🌐 ИНТЕРНЕТ: - search_web("query", results=5) - поиск в интернете - fetch_page("url") - загрузить страницу - analyze_website("url") - глубокий анализ сайта -""" - - tools_desc += """ -📁 ФАЙЛЫ: - read_file("path") - прочитать файл - save_file("path", content) - сохранить файл - list_files("dir", pattern="*") - список ф��йлов - analyze_file("path") - анализ файла - -🔔 УВЕДОМЛЕНИЯ: - notify_action("описание", severity="info|warning|critical") - важное действие -""" - - return f"{tools_desc}\n\n🎯 ЗАДАЧА:\n{task}" - - def _execute_with_interpreter(self, task: str, chat_id: str = None, mode: str = "chat") -> str: - if PROCESS_MANAGER.is_cancelled(): - return "🛑 Процесс отменён пользователем." - try: - configure_interpreter_for_model(self.model.name) - - # 🔥 Добавляем импорт интернет-агента в окружение интерпретатора - interpreter_prompt = f""" -{self.role.prompt} - -Доступные функции (ты можешь их вызывать): -- search_web(query, results=5) -> список результатов -- fetch_page(url) -> содержимое страницы -- analyze_website(url) -> анализ сайта -- read_file(path) -> содержимое файла -- save_file(path, content) -> сохранить файл - -{task} -""" - result = interpreter.chat(interpreter_prompt, display=False) - STATE.add_to_history(mode, "assistant", str(result)) - - if STATE.build_context.get("notifications", True): - if any(word in str(result).lower() for word in ['delete', 'remove', 'kill', 'rm']): - self.notifications.notify("Критическое действие в интерпретаторе", str(result)[:200], severity="critical") - return str(result) - except Exception as e: - error_msg = f"❌ Open Interpreter error: {e}" - STATE.add_to_history(mode, "assistant", error_msg) - return error_msg - - def _execute_with_api(self, task: str, sys_prompt_override: str = None, chat_id: str = None, history: List[Dict[str, str]] = None, mode: str = "chat") -> str: - if PROCESS_MANAGER.is_cancelled(): - return "🛑 Процесс отменён пользователем." - - system_prompt = sys_prompt_override or self.role.prompt - - # 🔥 Добавляем описание инструментов в системный промпт - internet_enabled = STATE.build_context.get("internet_access", True) - if internet_enabled: - system_prompt += """ - -Ты можешь использовать интернет для поиска информации: -- Когда нужно найти актуальные данные, используй поиск -- Когда нужно изучить сайт, используй анализ -- Результаты поиска помогут тебе дать более точный ответ -""" - - history = history or [] - messages = [{"role": "system", "content": system_prompt}] - for h in history[-10:]: - messages.append({"role": h["role"], "content": h["content"]}) - messages.append({"role": "user", "content": task}) - - if self.model.provider == "hf": - result = self._call_hf(messages, chat_id) - else: - result = self._call_openai_compatible(messages, chat_id) - - STATE.add_to_history(mode, "user", task) - STATE.add_to_history(mode, "assistant", result) - return result - - def _call_openai_compatible(self, messages: List[Dict], chat_id: str = None) -> str: - url = f"{API_BASE}/chat/completions" - data = json.dumps({"model": self.model.endpoint, "messages": messages, "temperature": 0.7, "max_tokens": self.model.max_tokens}).encode('utf-8') - req = urllib.request.Request(url, data=data, headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}) - try: - with urllib.request.urlopen(req, timeout=45) as response: - res = json.loads(response.read().decode('utf-8')) - return res['choices'][0]['message']['content'] - except Exception as primary_error: - return self._tiered_fallback(messages, primary_error, chat_id) - - def _call_hf(self, messages: List[Dict], chat_id: str = None) -> str: - url = "https://api-inference.huggingface.co/v1/chat/completions" - data = json.dumps({"model": self.model.endpoint, "messages": messages, "temperature": 0.7, "max_tokens": self.model.max_tokens}).encode('utf-8') - req = urllib.request.Request(url, data=data, headers={"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"}) - try: - with urllib.request.urlopen(req, timeout=60) as response: - res = json.loads(response.read().decode('utf-8')) - return res['choices'][0]['message']['content'] - except Exception as e: - return f"❌ HF API Error: {e}" - - def _tiered_fallback(self, messages: List[Dict], primary_error, chat_id: str = None) -> str: - if chat_id: - send_tg(chat_id, f"⚠️ Модель *{self.model.name}* упала. Перехожу на резерв...") - next_model_name = STATE.get_next_tier_model(self.model.name) - tried_models = [self.model.name] - while next_model_name: - tried_models.append(next_model_name) - next_model = STATE.models.get(next_model_name) - if not next_model: - break - try: - url = f"{API_BASE}/chat/completions" - data = json.dumps({"model": next_model.endpoint, "messages": messages, "temperature": 0.7, "max_tokens": next_model.max_tokens}).encode('utf-8') - req = urllib.request.Request(url, data=data, headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}) - with urllib.request.urlopen(req, timeout=45) as response: - res = json.loads(response.read().decode('utf-8')) - result = res['choices'][0]['message']['content'] - return result + f"\n\n_(🔄 Резерв: {self.model.name} → {next_model_name})_" - except Exception as e: - next_model_name = STATE.get_next_tier_model(next_model_name) - if not HF_TOKEN: - return f"❌ API Error: {primary_error}" - return self._fallback_hf(messages, primary_error, chat_id, tried_models) - - def _fallback_hf(self, messages: List[Dict], primary_error, chat_id: str = None, tried_models: List[str] = None) -> str: - tried_models = tried_models or [] - fallback = STATE.models.get("hf_fallback") - if not fallback: - return f"❌ API Error: {primary_error}" - try: - url = "https://api-inference.huggingface.co/v1/chat/completions" - data = json.dumps({"model": fallback.endpoint, "messages": messages, "temperature": 0.7, "max_tokens": fallback.max_tokens}).encode('utf-8') - req = urllib.request.Request(url, data=data, headers={"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"}) - with urllib.request.urlopen(req, timeout=60) as response: - res = json.loads(response.read().decode('utf-8')) - result = res['choices'][0]['message']['content'] - models_chain = " → ".join(tried_models + ["HF_fallback"]) - return result + f"\n\n_(🔄 Резервная цепочка: {models_chain})_" - except Exception as hf_error: - return f"❌ Все системы недоступны.\n1. API: {primary_error}\n2. HF: {hf_error}" -# ============================================================================ -# 🎼 CONDUCTOR ENGINE -# ============================================================================ - -class ConductorEngine: - def __init__(self, state: PinkSkyState): - self.state = state - self.use_interpreter = state.build_context.get("use_interpreter", True) - self.notifications = NOTIFICATIONS - - def orchestrate(self, user_request: str, chat_id: str = None, file_context: str = "", build_params: Dict[str, Any] = None) -> str: - if PROCESS_MANAGER.is_cancelled(): - return "🛑 Процесс отменён пользователем." - if chat_id: - self.notifications.set_chat_id(chat_id) - if self.state.build_context.get("notifications", True) and self.notifications.check_action(user_request): - self.notifications.notify("Build запущен", f"Запрос: {user_request[:100]}", severity="warning") - conductor = self.state.conductors.get(self.state.current_conductor, self.state.conductors["default"]) - if build_params: - self.state.build_context.update(build_params) - self.use_interpreter = self.state.build_context.get("use_interpreter", True) - self.state.add_to_history("build", "user", user_request) - plan = self._get_plan(conductor, user_request, file_context, chat_id) - if not plan: - error_msg = "❌ Кондуктор не смог создать план." - self.state.add_to_history("build", "assistant", error_msg) - return error_msg - results = self._execute_plan(plan, chat_id) - if len(results) == 1: - final = results[0] - self.state.add_to_history("build", "assistant", final) - return final - final = self._synthesize(conductor, user_request, results, chat_id) - self.state.add_to_history("build", "assistant", final) - return final - - def _get_plan(self, conductor: Conductor, user_request: str, file_context: str, chat_id: str = None) -> Optional[Dict]: - ctx = self.state.build_context - internet_status = "✅ Включён" if ctx.get("internet_access", True) else "❌ Отключён" - build_info = f"""🏗️ ПАРАМЕТРЫ БИЛДА: -- Агенты: {ctx['agents']} -- Тир: {ctx['models_tier']} -- OI: {'✅' if ctx.get('use_interpreter', True) else '❌'} -- Интернет: {internet_status} -- Стратегия: {ctx['strategy']}""" - full_prompt = f"{user_request}\n\n{file_context}\n\n{build_info}".strip() - rank_by = conductor.auto_rank_by - tier_map = {"tier1": 1, "tier2": 2, "tier3": 3, "tier4": 4, "tier5": 5} - max_tier = tier_map.get(ctx.get('models_tier', 'tier5'), 5) - model_name = self.state.get_best_model(rank_by=rank_by, max_tier=max_tier) - model = self.state.models.get(model_name, self.state.models["deepseek-v4-pro"]) - conductor_role = Role(name="conductor", prompt=conductor.prompt, description="Внутренняя роль кондуктора") - agent = UniversalAgent(conductor_role, model, use_interpreter=False) - roles_info = "\n".join([f"- {k}: {v.description}" for k, v in self.state.roles.items()]) - plan_prompt = f"""Пользовательский запрос: {full_prompt}\n\nДОСТУПНЫЕ РОЛИ:\n{roles_info}\n\nСоздай план выполнения в JSON формате:\n{{"strategy": "single|sequential|parallel", "tasks": [{{"role": "role_name", "model": "model_name", "prompt": "subtask"}}], "synthesis_prompt": "how to combine"}}""" - try: - plan_text = agent.execute(plan_prompt, chat_id=chat_id, mode="build") - json_match = re.search(r'\{.*\}', plan_text, re.DOTALL) - if json_match: - return json.loads(json_match.group()) - return {"strategy": ctx.get('strategy', 'single'), "tasks": [{"role": ctx.get('role', 'universal'), "model": self.state.get_model_for_role(ctx.get('role', 'universal')), "prompt": full_prompt}], "synthesis_prompt": ""} - except Exception as e: - return {"strategy": ctx.get('strategy', 'single'), "tasks": [{"role": ctx.get('role', 'universal'), "model": self.state.get_model_for_role(ctx.get('role', 'universal')), "prompt": full_prompt}], "synthesis_prompt": ""} - - def _execute_plan(self, plan: Dict, chat_id: str = None) -> List[str]: - tasks = plan.get("tasks", []) - strategy = plan.get("strategy", "single") - results = [] - interpreter_roles = ["guru", "hacker", "sdet", "qa", "evangelist"] - - if strategy == "parallel" and len(tasks) > 1: - threads = [] - result_container = {} - def run_task(idx, task): - if PROCESS_MANAGER.is_cancelled(): - result_container[idx] = "🛑 Процесс отменён пользователем." - return - role_name = task.get("role", self.state.current_role) - model_name = task.get("model", self.state.current_model) - prompt = task.get("prompt", "") - if model_name not in self.state.models: - model_name = self.state.get_model_for_role(role_name) - role = self.state.roles.get(role_name, self.state.roles["universal"]) - model = self.state.models.get(model_name, self.state.models["deepseek-v4-pro"]) - use_interpreter = self.use_interpreter and role_name in interpreter_roles - agent = UniversalAgent(role, model, use_interpreter=use_interpreter) - try: - result = agent.execute(prompt, chat_id=chat_id, history=self.state.build_history, mode="build") - result_container[idx] = result - except Exception as e: - result_container[idx] = f"❌ Задача {idx} ошибка: {e}" - for i, task in enumerate(tasks): - t = threading.Thread(target=run_task, args=(i, task)) - PROCESS_MANAGER.register_thread(t) - threads.append(t) - t.start() - for t in threads: - t.join(timeout=120) - PROCESS_MANAGER.clear() - results = [result_container.get(i, "⏱️ Таймаут") for i in range(len(tasks))] - else: - for task in tasks: - if PROCESS_MANAGER.is_cancelled(): - results.append("🛑 Процесс отменён пользователем.") - break - role_name = task.get("role", self.state.current_role) - model_name = task.get("model", self.state.current_model) - prompt = task.get("prompt", "") - if model_name not in self.state.models: - model_name = self.state.get_model_for_role(role_name) - role = self.state.roles.get(role_name, self.state.roles["universal"]) - model = self.state.models.get(model_name, self.state.models["deepseek-v4-pro"]) - use_interpreter = self.use_interpreter and role_name in interpreter_roles - agent = UniversalAgent(role, model, use_interpreter=use_interpreter) - try: - result = agent.execute(prompt, chat_id=chat_id, history=self.state.build_history, mode="build") - results.append(result) - except Exception as e: - results.append(f"❌ Ошибка: {e}") - return results - - def _synthesize(self, conductor: Conductor, original_request: str, results: List[str], chat_id: str = None) -> str: - synthesis_prompt = f"{conductor.prompt}\n\n🧩 Синтезируй результаты агентов.\nИсходный запрос: {original_request}\n\nРезультаты:\n" - for i, res in enumerate(results): - synthesis_prompt += f"\n--- Агент {i+1} ---\n{res}\n" - synth_model_name = self.state.get_best_model(rank_by="reasoning", max_tier=2) - model = self.state.models.get(synth_model_name, self.state.models["deepseek-v4-pro"]) - role = Role(name="synthesizer", prompt="Ты синтезируешь результаты агентов.", description="Синтезатор") - agent = UniversalAgent(role, model, use_interpreter=False) - try: - return agent.execute(synthesis_prompt, chat_id=chat_id, history=self.state.build_history, mode="build") - except Exception as e: - return "\n\n---\n".join(results) -# ============================================================================ -# 🎭 SKILL ORCHESTRATOR -# ============================================================================ - -class SkillOrchestrator: - def __init__(self, state: PinkSkyState): - self.state = state - - def execute_with_agents(self, chat_id: str, text: str, file_context: str = "", agents: List[str] = None) -> str: - if PROCESS_MANAGER.is_cancelled(): - return "🛑 Процесс отменён пользователем." - if agents is None or len(agents) == 0: - agents = self._auto_select_agents(text) - main_result = self._execute_main(text, file_context, chat_id) - if PROCESS_MANAGER.is_cancelled(): - return "🛑 Процесс отменён пользователем." - if agents and len(agents) > 1: - agent_results = self._run_agents_parallel(agents, text, main_result, chat_id) - if PROCESS_MANAGER.is_cancelled(): - return "🛑 Процесс отменён пользователем." - final = self._synthesize_results(text, main_result, agent_results, chat_id) - return final - return main_result - - def _auto_select_agents(self, text: str) -> List[str]: - agents = ["guru"] - if any(kw in text.lower() for kw in ["код", "программ", "функц", "класс"]): - agents.append("hacker") - if any(kw in text.lower() for kw in ["тест", "проверк", "валид"]): - agents.append("qa") - if any(kw in text.lower() for kw in ["архитект", "проект", "систем"]): - agents.append("architect") - if any(kw in text.lower() for kw in ["безопасн", "уязвим", "защит"]): - agents.append("critic") - if any(kw in text.lower() for kw in ["найди", "поиск", "узнай", "интернет"]): - agents.append("researcher") - return agents[:4] - - def _execute_main(self, text: str, file_context: str, chat_id: str) -> str: - if PROCESS_MANAGER.is_cancelled(): - return "🛑 Процесс отменён пользователем." - try: - configure_interpreter_for_model(STATE.current_model) - - # 🔥 Добавляем информацию о доступе к интернету - internet_status = "доступен" if STATE.build_context.get("internet_access", True) else "недоступен" - full_prompt = f""" -🔧 SKILL MODE — Исполнение кода - -🌐 Интернет: {internet_status} - -Доступные функции: -- search_web("query", results=5) -> поиск в интернете -- fetch_page("url") -> загрузить страницу -- analyze_website("url") -> анализ сайта - -{file_context} - -{text} -""" - result = interpreter.chat(full_prompt, display=False) - - if STATE.build_context.get("notifications", True): - if any(word in str(result).lower() for word in ['delete', 'remove', 'kill']): - NOTIFICATIONS.set_chat_id(chat_id) - NOTIFICATIONS.notify("Критическое действие в Skill Mode", str(result)[:200], severity="critical") - return str(result) - except Exception as e: - return f"❌ Open Interpreter error: {e}" - - def _run_agents_parallel(self, agents: List[str], text: str, main_result: str, chat_id: str) -> Dict[str, str]: - results = {} - threads = [] - result_lock = threading.Lock() - def run_agent(role_name: str): - if PROCESS_MANAGER.is_cancelled(): - with result_lock: - results[role_name] = "🛑 Процесс отменён пользователем." - return - role = STATE.roles.get(role_name) - if not role: - with result_lock: - results[role_name] = "❌ Роль не найдена" - return - model_name = STATE.get_model_for_role(role_name) - model = STATE.models.get(model_name) - agent = UniversalAgent(role, model, use_interpreter=False) - prompt = f"""Исходная задача: {text} - -Результат выполнения кода: -{main_result} - -Твоя роль: {role.prompt} - -Проанализируй результат. Найди проблемы, предложи улучшения. -Используй интернет если нужно для проверки информации.""" - try: - result = agent.execute(prompt, chat_id=chat_id, mode="skill") - with result_lock: - results[role_name] = result - except Exception as e: - with result_lock: - results[role_name] = f"❌ Ошибка: {e}" - for role_name in agents: - if role_name in STATE.roles and role_name != "universal": - t = threading.Thread(target=run_agent, args=(role_name,)) - PROCESS_MANAGER.register_thread(t) - threads.append(t) - t.start() - for t in threads: - t.join(timeout=60) - PROCESS_MANAGER.clear() - return results - - def _synthesize_results(self, text: str, main_result: str, agent_results: Dict[str, str], chat_id: str) -> str: - synthesis_prompt = f"""Исходная задача: {text} - -Основной результат (код): -{main_result} - -Анализ агентов: -""" - for role_name, result in agent_results.items(): - synthesis_prompt += f"\n--- {role_name.upper()} ---\n{result}\n" - synthesis_prompt += "\nСинтезируй финальный ответ: что сделано, проблемы, улучшения, итоговый код." - synth_model = STATE.get_best_model(rank_by="reasoning", max_tier=2) - model = STATE.models.get(synth_model) - role = Role(name="synthesizer", prompt="Ты синтезируешь результаты работы агентов.", description="Синтезатор") - agent = UniversalAgent(role, model, use_interpreter=False) - try: - return agent.execute(synthesis_prompt, chat_id=chat_id, mode="skill") - except Exception as e: - return f"❌ Ошибка синтеза: {e}" -# ============================================================================ -# 🔧 ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ -# ============================================================================ - -def configure_interpreter_for_model(model_name: str): - model = STATE.models.get(model_name) - if not model: - model = STATE.models["deepseek-v4-pro"] - interpreter.llm.model = f"openai/{model.endpoint}" - interpreter.llm.api_key = API_KEY - interpreter.llm.api_base = API_BASE - interpreter.llm.context_window = model.context_window - interpreter.llm.max_tokens = model.max_tokens - -def build_skill_prompt(new_request: str, file_context: str = "") -> str: - lines = [] - if STATE.skill_history: - lines.append("📜 ПРЕДЫДУЩИЕ ШАГИ:") - for entry in STATE.skill_history[-10:]: - role_emoji = "👤" if entry["role"] == "user" else "🤖" - content = entry["content"][:500] - lines.append(f"{role_emoji} {entry['role'].upper()}: {content}") - lines.append("") - if file_context: - lines.append(f"📎 КОНТЕКСТ ФАЙЛА:\n{file_context}\n") - lines.append(f"🎯 НОВЫЙ ЗАПРОС:\n{new_request}") - return "\n".join(lines) - -def parse_build_args(text: str) -> Tuple[Dict[str, Any], str]: - params = { - "agents": 3, - "models_tier": "tier1", - "skills_count": 2, - "files_count": 3, - "role": "universal", - "strategy": "parallel", - "use_interpreter": True, - "internet_access": True # 🔥 Новый параметр - } - words = text.split() - tz_words = [] - param_found = False - for word in words: - if "=" in word and not param_found: - key, val = word.split("=", 1) - if key in params: - if key in ["agents", "skills_count", "files_count"]: - try: - params[key] = int(val) - except: - pass - elif key in ["use_interpreter", "internet_access"]: - params[key] = val.lower() in ["true", "1", "yes", "on"] - else: - params[key] = val - else: - param_found = True - tz_words.append(word) - else: - param_found = True - tz_words.append(word) - return params, " ".join(tz_words) - -def parse_skill_args(text: str) -> Tuple[Dict[str, Any], str]: - params = {"skills_count": 2, "files_count": 3} - words = text.split() - content_words = [] - param_found = False - for word in words: - if "=" in word and not param_found: - key, val = word.split("=", 1) - if key in params: - try: - params[key] = int(val) - except: - pass - else: - param_found = True - content_words.append(word) - else: - param_found = True - content_words.append(word) - return params, " ".join(content_words) - -def parse_skill_agents(text: str) -> Tuple[List[str], str]: - parts = text.split(maxsplit=1) - if len(parts) < 2: - return [], "" - agents_part = parts[0] - rest = parts[1] if len(parts) > 1 else "" - agents = [a.strip() for a in agents_part.split(",") if a.strip()] - valid_agents = [a for a in agents if a in STATE.roles] - return valid_agents, rest - -def get_current_mode_info() -> str: - mode_emoji = {"chat": "💬", "skill": "🔧", "build": "🏗️", "paused": "⏸️"} - model = STATE.current_model - role = STATE.current_role - mode = STATE.current_mode - conductor = STATE.current_conductor - lines = [f"{mode_emoji.get(mode, '❓')} *Режим:* `{mode.upper()}`", f"🤖 *Модель:* `{model}`", f"👤 *Роль:* `{role}`"] - if mode == "build": - ctx = STATE.build_context - lines.append(f"🏗️ *Билд:* агенты={ctx['agents']}, OI={'✅' if ctx.get('use_interpreter', True) else '❌'}, Интернет={'✅' if ctx.get('internet_access', True) else '❌'}") - if mode == "skill": - ctx = STATE.build_context - lines.append(f"🔧 *Skill:* скиллы={ctx['skills_count']}, файлы={ctx['files_count']}, Интернет={'✅' if ctx.get('internet_access', True) else '❌'}") - return "\n".join(lines) -# ============================================================================ -# 📬 TELEGRAM INTEGRATION -# ============================================================================ - -def log(msg): - print(msg, flush=True) - -def send_tg(chat_id, text): - if not CF_URL or not TOKEN: - return - safe_text = str(text)[-4000:] - url = f"{CF_URL}/bot{TOKEN}/sendMessage" - data = json.dumps({"chat_id": chat_id, "text": safe_text, "parse_mode": "Markdown"}).encode('utf-8') - headers = {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"} - req = urllib.request.Request(url, data=data, headers=headers) - try: - urllib.request.urlopen(req, timeout=15) - except Exception as e: - log(f"❌ Ошибка отправки в TG: {e}") - -def download_tg_file(file_id, file_name, retries=3): - if not TOKEN: - return None - import urllib3 - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - base_api = CF_URL if CF_URL else "https://api.telegram.org" - get_file_url = f"{base_api}/bot{TOKEN}/getFile?file_id={file_id}" - for attempt in range(retries): - try: - res = requests.get(get_file_url, timeout=15, verify=False).json() - if not res.get("ok"): - return None - file_path = res["result"]["file_path"] - dl_url = f"{base_api}/file/bot{TOKEN}/{file_path}" - os.makedirs("downloads", exist_ok=True) - safe_name = os.path.basename(file_name) - local_path = os.path.join("downloads", safe_name) - response = requests.get(dl_url, stream=True, timeout=60, verify=False) - if response.status_code != 200: - direct_url = f"https://api.telegram.org/file/bot{TOKEN}/{file_path}" - response = requests.get(direct_url, stream=True, timeout=60, verify=False) - response.raise_for_status() - with open(local_path, "wb") as f: - for chunk in response.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - return local_path - except Exception as e: - time.sleep(2) - return None - -def send_tg_file(chat_id, file_path): - if not os.path.exists(file_path): - send_tg(chat_id, f"❌ Файл не найден: {file_path}") - return - base_api = CF_URL if CF_URL else "https://api.telegram.org" - url = f"{base_api}/bot{TOKEN}/sendDocument" - send_tg(chat_id, f"📎 Отправляю файл: *{os.path.basename(file_path)}*...") - try: - with open(file_path, 'rb') as f: - res = requests.post(url, data={'chat_id': chat_id}, files={'document': f}, verify=False, timeout=60) - if res.status_code == 200: - log(f"✅ Файл {file_path} отправлен в TG!") - else: - send_tg(chat_id, "❌ Не удалось отправить файл.") - except Exception as e: - send_tg(chat_id, f"❌ Ошибка отправки: {e}") - -def run_skill_mode(chat_id: str, text: str, file_context: str = ""): - params, clean_text = parse_skill_args(text) - STATE.build_context["skills_count"] = params["skills_count"] - STATE.build_context["files_count"] = params["files_count"] - STATE.add_to_history("skill", "user", clean_text) - configure_interpreter_for_model(STATE.current_model) - - # 🔥 Добавляем информацию о интернете - internet_status = "доступен" if STATE.build_context.get("internet_access", True) else "недоступен" - tools_prompt = f""" -🌐 Интернет: {internet_status} - -Доступные функции: -- search_web("query", results=5) -> поиск в интернете -- fetch_page("url") -> загрузить страницу -- analyze_website("url") -> анализ сайта -""" - - full_prompt = f"{tools_prompt}\n\n{file_context}\n\n{clean_text}" - - send_tg(chat_id, f"🔧 *Skill Mode* | Модель: `{STATE.current_model}` | Интернет: {'✅' if STATE.build_context.get('internet_access', True) else '❌'}") - - try: - result = interpreter.chat(full_prompt, display=False) - output = "\n".join([str(r) for r in result]) if isinstance(result, list) else str(result) - STATE.add_to_history("skill", "assistant", output) - if len(output) > 4000: - filename = f"skills/skill_result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md" - with open(filename, "w", encoding="utf-8") as f: - f.write(output) - send_tg(chat_id, f"✅ *Skill выполнен!*\n📄 Результат сохранён в файл.") - send_tg_file(chat_id, filename) - else: - send_tg(chat_id, f"✅ *Skill выполнен!*\n\n{output[:3900]}") - return output - except Exception as e: - error_msg = f"❌ Ошибка Skill Mode: {str(e)}" - STATE.add_to_history("skill", "assistant", error_msg) - send_tg(chat_id, error_msg) - return error_msg - -def run_chat_mode(chat_id: str, text: str, file_context: str = ""): - STATE.add_to_history("chat", "user", text) - model_name = STATE.current_model - role_name = STATE.current_role - model = STATE.models.get(model_name, STATE.models["deepseek-v4-pro"]) - role = STATE.roles.get(role_name, STATE.roles["universal"]) - agent = UniversalAgent(role, model, use_interpreter=False) - - # 🔥 Добавляем контекст интернета в задачу - internet_status = "доступен" if STATE.build_context.get("internet_access", True) else "недоступен" - task = f"🌐 Интернет: {internet_status}\n\n{text}" - if file_context: - task = f"📎 Контекст файла:\n{file_context}\n\n{task}" - - send_tg(chat_id, f"💬 *Chat Mode* | Модель: `{model_name}` | Интернет: {'✅' if STATE.build_context.get('internet_access', True) else '❌'}") - - try: - result = agent.execute(task=task, chat_id=chat_id, history=STATE.chat_history, mode="chat") - send_tg(chat_id, result[:3900] if len(result) > 4000 else result) - return result - except Exception as e: - error_msg = f"❌ Ошибка Chat Mode: {str(e)}" - STATE.add_to_history("chat", "assistant", error_msg) - send_tg(chat_id, error_msg) - return error_msg - send_tg(chat_id, "❌ Не указаны агенты") - self._ok() - return -# ============================================================================ -# 🤖 TELEGRAM HANDLER -# ============================================================================ - -class BotHandler(BaseHTTPRequestHandler): - def do_POST(self): - try: - content_length = int(self.headers.get('Content-Length', 0)) - post_data = self.rfile.read(content_length) - update = json.loads(post_data.decode('utf-8')) - if "message" not in update: - self._ok() - return - msg = update["message"] - chat_id = str(msg.get("chat", {}).get("id", "")) - text = msg.get("text", "").strip() - user_id = str(msg.get("from", {}).get("id", "")) - - if ALLOWED_USER and user_id not in ALLOWED_USER.split(","): - send_tg(chat_id, "🚫 Доступ запрещён.") - self._ok() - return - - file_context = "" - if "document" in msg: - file_id = msg["document"]["file_id"] - file_name = msg["document"].get("file_name", "file") - local_path = download_tg_file(file_id, file_name) - if local_path: - try: - with open(local_path, "r", encoding="utf-8") as f: - file_context = f.read()[:8000] - send_tg(chat_id, f"📎 Файл *{file_name}* загружен") - except: - send_tg(chat_id, f"📎 Файл *{file_name}* загружен (бинарный)") - - # ================================================================ - # 🆕 НОВЫЕ КОМАНДЫ - # ================================================================ - - # 🛑 /kill - if text == "/kill" or text == "/stop_all": - result = PROCESS_MANAGER.cancel_all() - send_tg(chat_id, f"🛑 {result}") - self._ok() - return - - # 🔍 /search - if text.startswith("/search"): - query = text[7:].strip() - if not query: - send_tg(chat_id, "🔍 Использование: `/search <запрос>`") - self._ok() - return - def do_search(): - results = INTERNET_AGENT.search_web(query) - if not results: - send_tg(chat_id, "🌐 Ничего не найдено") - return - output = f"🔍 *Результаты поиска:* `{query}`\n\n" - for i, r in enumerate(results, 1): - output += f"{i}. [{r['title']}]({r['url']})\n" - if r.get('snippet'): - output += f" _{r['snippet'][:100]}..._\n\n" - send_tg(chat_id, output[:3900]) - thread = threading.Thread(target=do_search) - PROCESS_MANAGER.register_thread(thread) - thread.start() - self._ok() - return - - # 🌐 /analyze - if text.startswith("/analyze"): - url = text[8:].strip() - if not url: - send_tg(chat_id, "🌐 Использование: `/analyze `") - self._ok() - return - def do_analyze(): - analysis = INTERNET_AGENT.analyze_website(url) - if 'error' in analysis: - send_tg(chat_id, f"❌ Ошибка: {analysis['error']}") - return - output = f"🌐 *Анализ сайта:* {analysis.get('title', url)}\n\n" - output += f"📊 Слов: {analysis.get('word_count', 0)}\n" - output += f"🔗 Ссылок: {len(analysis.get('links', []))}\n" - output += f"🖼️ Изображений: {len(analysis.get('images', []))}\n" - output += f"🌍 Язык: {analysis.get('language', 'unknown')}\n" - if analysis.get('keywords'): - output += f"\n🔑 Ключевые слова: {', '.join(analysis['keywords'][:10])}" - if analysis.get('meta_description'): - output += f"\n\n📝 Описание: {analysis['meta_description'][:200]}" - if analysis.get('headings'): - output += f"\n\n📑 Заголовки: {', '.join(analysis['headings'][:5])}" - send_tg(chat_id, output[:3900]) - thread = threading.Thread(target=do_analyze) - PROCESS_MANAGER.register_thread(thread) - thread.start() - self._ok() - return - - # 🗑️ /cache_clear - if text == "/cache_clear": - result = INTERNET_AGENT.clear_cache() - send_tg(chat_id, result) - self._ok() - return - - # 📊 /cache_stats - if text == "/cache_stats": - send_tg(chat_id, INTERNET_AGENT.get_cache_stats()) - self._ok() - return - - # 🔄 /internet - if text.startswith("/internet"): - parts = text.split() - if len(parts) < 2: - status = "✅ Включён" if STATE.build_context.get("internet_access", True) else "❌ Отключён" - send_tg(chat_id, f"🌐 Интернет: {status}\nИспользуй: `/internet on` или `/internet off`") - self._ok() - return - if parts[1] == "on": - STATE.build_context["internet_access"] = True - send_tg(chat_id, "🌐 Интернет включён для всех режимов!") - elif parts[1] == "off": - STATE.build_context["internet_access"] = False - send_tg(chat_id, "🌐 Интернет отключён для всех режимов!") - else: - send_tg(chat_id, "❌ Используй: `/internet on` или `/internet off`") - self._ok() - return - - # 📁 /files - if text.startswith("/files"): - parts = text.split(maxsplit=2) - if len(parts) < 2: - send_tg(chat_id, "📁 Использование:\n• `/files list `\n• `/files read `\n• `/files analyze `") - self._ok() - return - action = parts[1] - if action == "list": - directory = parts[2] if len(parts) > 2 else "." - files = FILE_MANAGER.list_files(directory, recursive=True) - if files: - result = f"📁 *Файлы в {directory}:*\n\n" + "\n".join([f"• `{f}`" for f in files[:30]]) - if len(files) > 30: - result += f"\n\n... и ещё {len(files) - 30} файлов" - send_tg(chat_id, result) - else: - send_tg(chat_id, f"📁 Нет файлов в {directory}") - elif action == "read": - if len(parts) < 3: - send_tg(chat_id, "📁 Использование: `/files read `") - else: - path = parts[2] - content, file_type = FILE_MANAGER.read_file(path) - if content: - if len(content) > 4000: - content = content[:4000] + "\n\n... (файл обрезан)" - send_tg(chat_id, f"📄 *{path}* ({file_type})\n\n```\n{content}\n```") - else: - send_tg(chat_id, f"❌ Файл не найден: {path}") - elif action == "analyze": - if len(parts) < 3: - send_tg(chat_id, "📁 Использование: `/files analyze `") - else: - path = parts[2] - analysis = FILE_MANAGER.analyze_file(path) - if "error" in analysis: - send_tg(chat_id, f"❌ {analysis['error']}") - else: - result = f"📊 *Анализ файла:* {analysis['name']}\n\n" - result += f"📏 Размер: {analysis['size_bytes']} байт\n" - result += f"📂 Тип: {analysis['type']}\n" - if 'lines' in analysis: - result += f"📝 Строк: {analysis['lines']}\n" - send_tg(chat_id, result) - self._ok() - return - - # 🔔 /notifications - if text.startswith("/notifications"): - parts = text.split() - if len(parts) < 2: - send_tg(chat_id, "🔔 Использование:\n• `/notifications on`\n• `/notifications off`\n• `/notifications history`") - self._ok() - return - action = parts[1] - if action == "on": - STATE.build_context["notifications"] = True - NOTIFICATIONS.set_enabled(True) - send_tg(chat_id, "🔔 Уведомления включены!") - elif action == "off": - STATE.build_context["notifications"] = False - NOTIFICATIONS.set_enabled(False) - send_tg(chat_id, "🔇 Уведомления выключены!") - elif action == "history": - send_tg(chat_id, NOTIFICATIONS.get_history()) - self._ok() - return - - # 🎭 /skill_agents - if text.startswith("/skill_agents"): - rest = text[len("/skill_agents"):].strip() - agents, skill_text = parse_skill_agents(rest) - if not skill_text: - send_tg(chat_id, "🎭 Использование: `/skill_agents guru,hacker,critic <запрос>`") - self._ok() - return - if not agents: - send_tg(chat_id, "❌ Не указаны агенты") - self._ok() - return - orchestrator = SkillOrchestrator(STATE) - thread = threading.Thread(target=lambda: send_tg(chat_id, orchestrator.execute_with_agents(chat_id, skill_text, file_context, agents))) - PROCESS_MANAGER.register_thread(thread) - thread.start() - self._ok() - return - - # 🏗️ /build_mode - if text.startswith("/build_mode"): - parts = text.split() - editor = BuildModeEditor(STATE) - if len(parts) < 2: - help_text = "����️ *Build Mode Editor*\n\n• `/build_mode list`\n• `/build_mode show `\n• `/build_mode save `\n• `/build_mode load `\n• `/build_mode delete `" - send_tg(chat_id, help_text) - self._ok() - return - action = parts[1] - if action == "list": - send_tg(chat_id, editor.list_modes()) - elif action == "show" and len(parts) >= 3: - send_tg(chat_id, editor.show_mode(parts[2])) - elif action == "save" and len(parts) >= 3: - send_tg(chat_id, editor.save_mode(parts[2])) - elif action == "load" and len(parts) >= 3: - send_tg(chat_id, editor.load_mode(parts[2])) - elif action == "delete" and len(parts) >= 3: - send_tg(chat_id, editor.delete_mode(parts[2])) - else: - send_tg(chat_id, f"❌ Неизвестная команда: {action}") - self._ok() - return - - # 🔄 /mode - if text.startswith("/mode"): - parts = text.split() - if len(parts) >= 2: - mode_name = parts[1].lower() - if mode_name in ["chat", "skill", "build", "paused"]: - STATE.current_mode = mode_name - STATE.cancel_flag = False - send_tg(chat_id, f"🔄 Режим изменён на: *{mode_name.upper()}*") - if mode_name == "build": - ctx = STATE.build_context - send_tg(chat_id, f"🏗️ *Build контекст:*\n• Агенты: {ctx['agents']}\n• OI: {'✅' if ctx.get('use_interpreter', True) else '❌'}\n• Интернет: {'✅' if ctx.get('internet_access', True) else '❌'}") - elif mode_name == "skill": - ctx = STATE.build_context - send_tg(chat_id, f"🔧 *Skill контекст:*\n• Скиллы: {ctx['skills_count']}\n• Файлы: {ctx['files_count']}\n• Интернет: {'✅' if ctx.get('internet_access', True) else '❌'}") - else: - send_tg(chat_id, f"❌ Режим `{mode_name}` не найден. Доступны: chat, skill, build, paused") - else: - send_tg(chat_id, f"ℹ️ Текущий режим: `{STATE.current_mode.upper()}`") - self._ok() - return - - # ================================================================ - # 🤖 /model - # ================================================================ - - if text.startswith("/model"): - parts = text.split() - if len(parts) >= 2: - model_name = parts[1] - if model_name in STATE.models: - STATE.current_model = model_name - model = STATE.models[model_name] - tags_str = ", ".join(model.tags[:3]) if model.tags else "базовая" - send_tg(chat_id, f"🤖 *Модель изменена:* `{model_name}`\n📊 *Теги:* {tags_str}") - else: - available_list = [] - for k in STATE.models.keys(): - if k != "hf_fallback": - model = STATE.models[k] - tags_str = ", ".join(model.tags[:3]) if model.tags else "базовая" - available_list.append(f"• `{k}` — {tags_str}") - available = "\n".join(available_list[:20]) - send_tg(chat_id, f"❌ Модель `{model_name}` не найдена.\n\n*Доступные модели:*\n{available}") - else: - available_list = [] - for k in STATE.models.keys(): - if k != "hf_fallback": - model = STATE.models[k] - tags_str = ", ".join(model.tags[:3]) if model.tags else "базовая" - available_list.append(f"• `{k}` — {tags_str}") - available = "\n".join(available_list[:20]) - send_tg(chat_id, f"🤖 *Текущая модель:* `{STATE.current_model}`\n\n*Доступные модели:*\n{available}") - self._ok() - return - - # ================================================================ - # 👤 /role - # ================================================================ - - if text.startswith("/role"): - parts = text.split() - if len(parts) >= 2: - role_name = parts[1] - if role_name in STATE.roles: - STATE.current_role = role_name - role = STATE.roles[role_name] - send_tg(chat_id, f"👤 *Личность изменена:* `{role_name}`\n📝 *Описание:* {role.description}") - else: - available = "\n".join([f"• `{k}` — {v.description}" for k, v in STATE.roles.items()]) - send_tg(chat_id, f"❌ Личность `{role_name}` не найдена.\n\n*Доступные личности:*\n{available}") - else: - available = "\n".join([f"• `{k}` — {v.description}" for k, v in STATE.roles.items()]) - send_tg(chat_id, f"👤 *Текущая личность:* `{STATE.current_role}`\n\n*Доступные личности:*\n{available}") - self._ok() - return - - # ================================================================ - # 🎼 /conductor - # ================================================================ - - if text.startswith("/conductor"): - parts = text.split() - if len(parts) >= 2: - cond_name = parts[1] - if cond_name in STATE.conductors: - STATE.current_conductor = cond_name - cond = STATE.conductors[cond_name] - send_tg(chat_id, f"🎼 *Кондуктор изменён:* `{cond_name}`\n📝 *Описание:* {cond.description}") - else: - available = "\n".join([f"• `{k}` — {v.description}" for k, v in STATE.conductors.items()]) - send_tg(chat_id, f"❌ Кондуктор `{cond_name}` не найден.\n\n*Доступные кондукторы:*\n{available}") - else: - available = "\n".join([f"• `{k}` — {v.description}" for k, v in STATE.conductors.items()]) - send_tg(chat_id, f"🎼 *Текущий кондуктор:* `{STATE.current_conductor}`\n\n*Доступные кондукторы:*\n{available}") - self._ok() - return - - # ================================================================ - # СТАНДАРТНЫЕ КОМАНДЫ - # ================================================================ - - if text == "/start": - STATE.current_mode = "chat" - STATE.cancel_flag = False - welcome = ( - "🧠 *PinkSky v6.0* — Полный доступ в интернет!\n\n" - "📋 *КОМАНДЫ:*\n" - "🤖 `/model [name]` — смена модели\n" - "👤 `/role [name]` — смена личности\n" - "🎼 `/conductor [name]` — смена кондуктора\n" - "🔄 `/mode [chat|skill|build|paused]` — смена режима\n" - "🔍 `/search <запрос>` — поиск в интернете\n" - "🌐 `/analyze ` — анализ сайта\n" - "📁 `/files list/read/analyze` — файлы\n" - "🔔 `/notifications on/off` — уведомления\n" - "🔄 `/internet on/off` — интернет\n" - "🗑️ `/cache_clear` — очистить кэш\n" - "📊 `/cache_stats` — статистика кэша\n" - "🎭 `/skill_agents g,h,c <запрос>` — агенты\n" - "🏗️ `/build_mode list/show/save/load` — режимы\n" - "🛑 `/kill` — остановить процессы\n" - "⏸️ `/stop` — пауза\n\n" - f"{get_current_mode_info()}" - ) - send_tg(chat_id, welcome) - self._ok() - return - - if text == "/stop": - STATE.current_mode = "paused" - STATE.cancel_flag = True - send_tg(chat_id, "⏸️ *ПАУЗА*\nБот остановлен. /start чтобы продолжить.") - self._ok() - return - - if text == "/status": - send_tg(chat_id, get_current_mode_info()) - send_tg(chat_id, f"🔢 Активных процессов: {PROCESS_MANAGER.get_active_count()}") - self._ok() - return - - if text.startswith("/history"): - parts = text.split() - fmt = parts[1] if len(parts) > 1 else "md" - if fmt == "json": - content = STATE.export_history_json() - filename = f"prompts/history_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - with open(filename, "w", encoding="utf-8") as f: - f.write(content) - send_tg(chat_id, "📜 *История (JSON)*") - send_tg_file(chat_id, filename) - else: - content = STATE.export_history_md() - filename = f"prompts/history_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md" - with open(filename, "w", encoding="utf-8") as f: - f.write(content) - send_tg(chat_id, "📜 *История (MD)*") - send_tg_file(chat_id, filename) - self._ok() - return - - if text.startswith("/skill"): - STATE.current_mode = "skill" - clean_text = text[6:].strip() - if not clean_text: - send_tg(chat_id, "🔧 Использование: `/skill [skills=N] [files=N] <запрос>`") - self._ok() - return - thread = threading.Thread(target=run_skill_mode, args=(chat_id, clean_text, file_context)) - PROCESS_MANAGER.register_thread(thread) - thread.start() - self._ok() - return - - if text.startswith("/build"): - STATE.current_mode = "build" - clean_text = text[6:].strip() - if not clean_text: - send_tg(chat_id, "🏗️ Использование: `/build [agents=N] [use_interpreter=true] [internet_access=true] <ТЗ>`") - self._ok() - return - params, tz_text = parse_build_args(clean_text) - STATE.build_context["use_interpreter"] = params.get("use_interpreter", True) - STATE.build_context["internet_access"] = params.get("internet_access", True) - engine = ConductorEngine(STATE) - thread = threading.Thread(target=lambda: send_tg(chat_id, engine.orchestrate(user_request=tz_text, chat_id=chat_id, file_context=file_context, build_params=params))) - PROCESS_MANAGER.register_thread(thread) - thread.start() - self._ok() - return - - # ================================================================ - # ОБЫЧНЫЕ СООБЩЕНИЯ (БЕЗ КОМАНД) - # ================================================================ - - if not text: - self._ok() - return - - if STATE.current_mode == "paused": - send_tg(chat_id, "⏸️ Бот в паузе. /start чтобы продолжить.") - self._ok() - return - - if STATE.current_mode == "skill": - thread = threading.Thread(target=run_skill_mode, args=(chat_id, text, file_context)) - PROCESS_MANAGER.register_thread(thread) - thread.start() - self._ok() - return - - if STATE.current_mode == "build": - engine = ConductorEngine(STATE) - thread = threading.Thread(target=lambda: send_tg(chat_id, engine.orchestrate(user_request=text, chat_id=chat_id, file_context=file_context))) - PROCESS_MANAGER.register_thread(thread) - thread.start() - self._ok() - return - - # По умолчанию — чат-режим - thread = threading.Thread(target=run_chat_mode, args=(chat_id, text, file_context)) - PROCESS_MANAGER.register_thread(thread) - thread.start() - self._ok() - - except Exception as e: - log(f"❌ Ошибка: {e}") - self._ok() - - def do_GET(self): - self.send_response(200) - self.end_headers() - self.wfile.write(b"PinkSky v6.0 is running!") - - def _ok(self): - self.send_response(200) - self.end_headers() - self.wfile.write(b"OK") - - def log_message(self, format, *args): - pass -# ============================================================================ -# 🚀 MAIN -# ============================================================================ - -def main(): - log("=" * 60) - log("🧠 PINKSKY v6.0 — ПОЛНЫЙ ДОСТУП В ИНТЕРНЕТ") - log("=" * 60) - if not TOKEN: - log("❌ TELEGRAM_BOT_TOKEN не найден!") - sys.exit(1) - if not API_KEY: - log("⚠️ NVIDIA_API_KEY не найден") - log(f"📊 Загружено моделей: {len(STATE.models)}") - log(f"📊 Загружено ролей: {len(STATE.roles)}") - log(f"📊 Загружено кондукторов: {len(STATE.conductors)}") - log(f"🌐 Интернет: {'✅' if STATE.build_context.get('internet_access', True) else '❌'}") - log(f"🔧 Open Interpreter: {'✅' if STATE.build_context.get('use_interpreter', True) else '❌'}") - log(f"🔔 Уведомления: {'✅' if STATE.build_context.get('notifications', True) else '❌'}") - log(f"🗑️ Кэш: {INTERNET_AGENT.get_cache_stats()}") - STATE.current_mode = "chat" - server = HTTPServer(("0.0.0.0", PORT), BotHandler) - log(f"🚀 Сервер запущен на порту {PORT}") - log("=" * 60) - try: - server.serve_forever() - except KeyboardInterrupt: - log("\n🛑 Остановка...") - PROCESS_MANAGER.cancel_all() - STATE.save_history() - log("💾 История сохранена.") - server.shutdown() - -if __name__ == "__main__": - main() \ No newline at end of file