Spaces:
Runtime error
Runtime error
Upload 19 files
Browse files- __init__.py +11 -0
- build_mode_editor.py +68 -0
- conductor_engine.py +163 -0
- config.py +65 -0
- file_manager.py +104 -0
- helpers.py +58 -0
- internet_agent.py +259 -0
- main.py +45 -0
- mode_handlers.py +101 -0
- model_ranking.py +192 -0
- models.py +39 -0
- notification_system.py +85 -0
- process_manager.py +67 -0
- routes.py +24 -0
- skill_orchestrator.py +87 -0
- state.py +466 -0
- telegram_handlers.py +316 -0
- telegram_utils.py +69 -0
- universal_agent.py +145 -0
__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PinkSky v7.0 — модульная архитектура"""
|
| 2 |
+
|
| 3 |
+
from .main import main
|
| 4 |
+
from .config import PORT, TOKEN, API_KEY, API_BASE, HF_TOKEN
|
| 5 |
+
from .state import STATE
|
| 6 |
+
from .process_manager import PROCESS_MANAGER
|
| 7 |
+
from .internet_agent import INTERNET_AGENT
|
| 8 |
+
from .notification_system import NOTIFICATIONS
|
| 9 |
+
|
| 10 |
+
__version__ = "7.0.0"
|
| 11 |
+
__all__ = ["main", "STATE", "PROCESS_MANAGER", "INTERNET_AGENT", "NOTIFICATIONS"]
|
build_mode_editor.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build Mode Editor — управление режимами сборки"""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
from typing import Dict
|
| 6 |
+
from .config import BUILD_MODES_FILE
|
| 7 |
+
|
| 8 |
+
class BuildModeEditor:
|
| 9 |
+
def __init__(self, state):
|
| 10 |
+
self.state = state
|
| 11 |
+
|
| 12 |
+
def list_modes(self) -> str:
|
| 13 |
+
modes = self._load_all_modes()
|
| 14 |
+
if not modes:
|
| 15 |
+
return "📋 Нет сохранённых режимов сборки"
|
| 16 |
+
lines = ["📋 Режимы сборки:"]
|
| 17 |
+
for name, config in modes.items():
|
| 18 |
+
lines.append(f"- {name}: {config.get('description', 'No description')}")
|
| 19 |
+
return "\n".join(lines)
|
| 20 |
+
|
| 21 |
+
def show_mode(self, mode_name: str) -> str:
|
| 22 |
+
modes = self._load_all_modes()
|
| 23 |
+
mode = modes.get(mode_name)
|
| 24 |
+
if not mode:
|
| 25 |
+
return f"❌ Режим '{mode_name}' не найден"
|
| 26 |
+
return f"📋 {mode_name}:\n```json\n{json.dumps(mode, ensure_ascii=False, indent=2)}\n```"
|
| 27 |
+
|
| 28 |
+
def save_mode(self, mode_name: str, config: Dict = None) -> str:
|
| 29 |
+
modes = self._load_all_modes()
|
| 30 |
+
if config is None:
|
| 31 |
+
config = dict(self.state.build_context)
|
| 32 |
+
config["description"] = f"Mode created {datetime.now().isoformat()}"
|
| 33 |
+
modes[mode_name] = config
|
| 34 |
+
try:
|
| 35 |
+
with open(BUILD_MODES_FILE, "w", encoding="utf-8") as f:
|
| 36 |
+
json.dump(modes, f, ensure_ascii=False, indent=2)
|
| 37 |
+
return f"✅ Режим '{mode_name}' сохранён"
|
| 38 |
+
except Exception as e:
|
| 39 |
+
return f"❌ Ошибка сохранения: {e}"
|
| 40 |
+
|
| 41 |
+
def load_mode(self, mode_name: str) -> str:
|
| 42 |
+
modes = self._load_all_modes()
|
| 43 |
+
mode = modes.get(mode_name)
|
| 44 |
+
if not mode:
|
| 45 |
+
return f"❌ Режим '{mode_name}' не найден"
|
| 46 |
+
self.state.build_context.update(mode)
|
| 47 |
+
return f"✅ Режим '{mode_name}' загружен"
|
| 48 |
+
|
| 49 |
+
def delete_mode(self, mode_name: str) -> str:
|
| 50 |
+
modes = self._load_all_modes()
|
| 51 |
+
if mode_name not in modes:
|
| 52 |
+
return f"❌ Режим '{mode_name}' не найден"
|
| 53 |
+
del modes[mode_name]
|
| 54 |
+
try:
|
| 55 |
+
with open(BUILD_MODES_FILE, "w", encoding="utf-8") as f:
|
| 56 |
+
json.dump(modes, f, ensure_ascii=False, indent=2)
|
| 57 |
+
return f"🗑️ Режим '{mode_name}' удалён"
|
| 58 |
+
except Exception as e:
|
| 59 |
+
return f"❌ Ошибка удаления: {e}"
|
| 60 |
+
|
| 61 |
+
def _load_all_modes(self) -> Dict:
|
| 62 |
+
if not os.path.exists(BUILD_MODES_FILE):
|
| 63 |
+
return {}
|
| 64 |
+
try:
|
| 65 |
+
with open(BUILD_MODES_FILE, "r", encoding="utf-8") as f:
|
| 66 |
+
return json.load(f)
|
| 67 |
+
except:
|
| 68 |
+
return {}
|
conductor_engine.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Conductor Engine — оркестрация Build режима"""
|
| 2 |
+
|
| 3 |
+
import threading
|
| 4 |
+
import json
|
| 5 |
+
import re
|
| 6 |
+
from typing import Dict, List, Optional, Any
|
| 7 |
+
from .models import Role, Conductor
|
| 8 |
+
from .universal_agent import UniversalAgent
|
| 9 |
+
from .state import STATE
|
| 10 |
+
from .process_manager import PROCESS_MANAGER
|
| 11 |
+
from .notification_system import NOTIFICATIONS
|
| 12 |
+
|
| 13 |
+
class ConductorEngine:
|
| 14 |
+
def __init__(self, state):
|
| 15 |
+
self.state = state
|
| 16 |
+
self.use_interpreter = state.build_context.get("use_interpreter", True)
|
| 17 |
+
self.notifications = NOTIFICATIONS
|
| 18 |
+
|
| 19 |
+
def orchestrate(self, user_request: str, chat_id: str = None, file_context: str = "",
|
| 20 |
+
build_params: Dict[str, Any] = None) -> str:
|
| 21 |
+
conductor = self.state.conductors.get(self.state.current_conductor, self.state.conductors["default"])
|
| 22 |
+
plan = self._get_plan(conductor, user_request, file_context, chat_id)
|
| 23 |
+
if not plan:
|
| 24 |
+
return "❌ Conductor не смог создать план."
|
| 25 |
+
results = self._execute_plan(plan, chat_id)
|
| 26 |
+
if len(results) == 1:
|
| 27 |
+
return results[0]
|
| 28 |
+
return self._synthesize(conductor, user_request, results, chat_id)
|
| 29 |
+
|
| 30 |
+
def _get_plan(self, conductor: Conductor, user_request: str, file_context: str, chat_id: str = None) -> Optional[Dict]:
|
| 31 |
+
full_prompt = f"{user_request}\n\n{file_context}".strip()
|
| 32 |
+
rank_by = conductor.auto_rank_by
|
| 33 |
+
if conductor.cost_aware and rank_by == "coding":
|
| 34 |
+
rank_by = "balanced"
|
| 35 |
+
model_name = self.state.get_best_model(rank_by=rank_by, max_tier=2)
|
| 36 |
+
model = self.state.models.get(model_name, self.state.models["deepseek-v4-pro"])
|
| 37 |
+
conductor_role = Role(name="conductor", prompt=conductor.prompt, description="Internal conductor role")
|
| 38 |
+
agent = UniversalAgent(conductor_role, model)
|
| 39 |
+
|
| 40 |
+
roles_info = "\n".join([f"- {k}: {v.description} (complexity: {v.complexity}, preferred: {', '.join(v.preferred_models)})"
|
| 41 |
+
for k, v in self.state.roles.items()])
|
| 42 |
+
models_info = "\n".join([f"- {k}: coding_rank={v.coding_rank}, speed_rank={v.speed_rank}, reasoning_rank={v.reasoning_rank}, cost=${v.cost_per_1k_output}/1k"
|
| 43 |
+
for k, v in sorted(self.state.models.items(), key=lambda x: x[1].coding_rank) if k != "hf_fallback"])
|
| 44 |
+
|
| 45 |
+
plan_prompt = f"""User request: {full_prompt}
|
| 46 |
+
|
| 47 |
+
AVAILABLE ROLES:
|
| 48 |
+
{roles_info}
|
| 49 |
+
|
| 50 |
+
AVAILABLE MODELS (sorted by coding rank):
|
| 51 |
+
{models_info}
|
| 52 |
+
|
| 53 |
+
Current selection criteria: {conductor.auto_rank_by}
|
| 54 |
+
Cost-aware: {conductor.cost_aware}
|
| 55 |
+
|
| 56 |
+
Create execution plan."""
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
plan_text = agent.execute(plan_prompt)
|
| 60 |
+
json_match = re.search(r'\{.*\}', plan_text, re.DOTALL)
|
| 61 |
+
if json_match:
|
| 62 |
+
return json.loads(json_match.group())
|
| 63 |
+
else:
|
| 64 |
+
return {
|
| 65 |
+
"strategy": "single",
|
| 66 |
+
"tasks": [{"role": self.state.current_role, "model": self.state.get_model_for_role(self.state.current_role), "prompt": full_prompt}],
|
| 67 |
+
"synthesis_prompt": ""
|
| 68 |
+
}
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f"Planning error: {e}")
|
| 71 |
+
return {
|
| 72 |
+
"strategy": "single",
|
| 73 |
+
"tasks": [{"role": self.state.current_role, "model": self.state.get_model_for_role(self.state.current_role), "prompt": full_prompt}],
|
| 74 |
+
"synthesis_prompt": ""
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
def _execute_plan(self, plan: Dict, chat_id: str = None) -> List[str]:
|
| 78 |
+
tasks = plan.get("tasks", [])
|
| 79 |
+
strategy = plan.get("strategy", "single")
|
| 80 |
+
results = []
|
| 81 |
+
|
| 82 |
+
if strategy == "parallel" and len(tasks) > 1:
|
| 83 |
+
threads = []
|
| 84 |
+
result_container = {}
|
| 85 |
+
|
| 86 |
+
def run_task(idx, task):
|
| 87 |
+
if PROCESS_MANAGER.is_cancelled():
|
| 88 |
+
result_container[idx] = "Cancelled"
|
| 89 |
+
return
|
| 90 |
+
role_name = task.get("role", self.state.current_role)
|
| 91 |
+
model_name = task.get("model", self.state.current_model)
|
| 92 |
+
prompt = task.get("prompt", "")
|
| 93 |
+
if model_name not in self.state.models:
|
| 94 |
+
model_name = self.state.get_model_for_role(role_name)
|
| 95 |
+
role = self.state.roles.get(role_name, self.state.roles["universal"])
|
| 96 |
+
model = self.state.models.get(model_name, self.state.models["deepseek-v4-pro"])
|
| 97 |
+
use_interp = self._should_use_interpreter(role_name, prompt)
|
| 98 |
+
agent = UniversalAgent(role, model, use_interpreter=use_interp)
|
| 99 |
+
try:
|
| 100 |
+
result = agent.execute(prompt, chat_id=chat_id)
|
| 101 |
+
result_container[idx] = result
|
| 102 |
+
except Exception as e:
|
| 103 |
+
result_container[idx] = f"Task {idx} error: {e}"
|
| 104 |
+
|
| 105 |
+
for i, task in enumerate(tasks):
|
| 106 |
+
t = threading.Thread(target=run_task, args=(i, task))
|
| 107 |
+
threads.append(t)
|
| 108 |
+
PROCESS_MANAGER.register_thread(t)
|
| 109 |
+
t.start()
|
| 110 |
+
for t in threads:
|
| 111 |
+
t.join(timeout=120)
|
| 112 |
+
results = [result_container.get(i, "Timeout") for i in range(len(tasks))]
|
| 113 |
+
else:
|
| 114 |
+
for task in tasks:
|
| 115 |
+
if PROCESS_MANAGER.is_cancelled():
|
| 116 |
+
results.append("Cancelled")
|
| 117 |
+
continue
|
| 118 |
+
role_name = task.get("role", self.state.current_role)
|
| 119 |
+
model_name = task.get("model", self.state.current_model)
|
| 120 |
+
prompt = task.get("prompt", "")
|
| 121 |
+
if model_name not in self.state.models:
|
| 122 |
+
model_name = self.state.get_model_for_role(role_name)
|
| 123 |
+
role = self.state.roles.get(role_name, self.state.roles["universal"])
|
| 124 |
+
model = self.state.models.get(model_name, self.state.models["deepseek-v4-pro"])
|
| 125 |
+
use_interp = self._should_use_interpreter(role_name, prompt)
|
| 126 |
+
agent = UniversalAgent(role, model, use_interpreter=use_interp)
|
| 127 |
+
try:
|
| 128 |
+
result = agent.execute(prompt, chat_id=chat_id)
|
| 129 |
+
results.append(result)
|
| 130 |
+
except Exception as e:
|
| 131 |
+
results.append(f"Error: {e}")
|
| 132 |
+
return results
|
| 133 |
+
|
| 134 |
+
def _should_use_interpreter(self, role_name: str, task: str) -> bool:
|
| 135 |
+
code_roles = ["guru", "hacker", "sdet", "qa", "evangelist"]
|
| 136 |
+
if role_name in code_roles:
|
| 137 |
+
return True
|
| 138 |
+
code_keywords = ["код", "напиши", "создай", "файл", "исполни", "запусти", "отладить", "исправить", "проверить", "тест"]
|
| 139 |
+
if any(kw in task.lower() for kw in code_keywords):
|
| 140 |
+
return True
|
| 141 |
+
if "тест" in task.lower() or "проверк" in task.lower():
|
| 142 |
+
return True
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
def _synthesize(self, conductor: Conductor, original_request: str, results: List[str], chat_id: str = None) -> str:
|
| 146 |
+
synthesis_prompt = conductor.prompt + f"""
|
| 147 |
+
|
| 148 |
+
Synthesize multiple agent results into a single answer.
|
| 149 |
+
|
| 150 |
+
Original request: {original_request}
|
| 151 |
+
|
| 152 |
+
Agent results:
|
| 153 |
+
"""
|
| 154 |
+
for i, res in enumerate(results):
|
| 155 |
+
synthesis_prompt += f"\n--- Agent {i+1} result ---\n{res}\n"
|
| 156 |
+
synthesis_role = Role(name="synthesizer", prompt="You synthesize agent results.", description="Synthesizer")
|
| 157 |
+
synth_model_name = self.state.get_best_model(rank_by="reasoning", max_tier=2)
|
| 158 |
+
model = self.state.models.get(synth_model_name, self.state.models["deepseek-v4-pro"])
|
| 159 |
+
agent = UniversalAgent(synthesis_role, model)
|
| 160 |
+
try:
|
| 161 |
+
return agent.execute(synthesis_prompt, chat_id=chat_id)
|
| 162 |
+
except Exception as e:
|
| 163 |
+
return "\n\n---\n".join(results)
|
config.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Конфигурация приложения и DNS resolver"""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
import urllib.request
|
| 6 |
+
import socket
|
| 7 |
+
|
| 8 |
+
# === ENVIRONMENT CONFIG ===
|
| 9 |
+
PORT = 7860
|
| 10 |
+
TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
|
| 11 |
+
ALLOWED_USER = os.environ.get("TELEGRAM_ALLOWED_USERS", "").strip()
|
| 12 |
+
CF_URL = os.environ.get("CF_WORKER_URL", "").rstrip('/')
|
| 13 |
+
|
| 14 |
+
# NVIDIA API
|
| 15 |
+
API_KEY = os.environ.get("NVIDIA_API_KEY", os.environ.get("OPENAI_API_KEY", "")).strip()
|
| 16 |
+
API_BASE = os.environ.get("NVIDIA_API_BASE", os.environ.get("OPENAI_API_BASE", "")).strip().rstrip('/')
|
| 17 |
+
if API_BASE and not API_BASE.endswith("/v1"):
|
| 18 |
+
API_BASE += "/v1"
|
| 19 |
+
|
| 20 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
|
| 21 |
+
HF_FALLBACK_MODEL = "Qwen/Qwen2.5-72B-Instruct"
|
| 22 |
+
|
| 23 |
+
# === FILE PATHS ===
|
| 24 |
+
ROLES_FILE = "prompts/roles.json"
|
| 25 |
+
MODELS_FILE = "prompts/models.json"
|
| 26 |
+
CONDUCTORS_FILE = "prompts/conductors.json"
|
| 27 |
+
HISTORY_FILE = "prompts/history.json"
|
| 28 |
+
BUILD_MODES_FILE = "prompts/build_modes.json"
|
| 29 |
+
|
| 30 |
+
# === CUSTOM DNS RESOLVER ===
|
| 31 |
+
HF_DOMAIN = "api-inference.huggingface.co"
|
| 32 |
+
resolved_hf_ip = None
|
| 33 |
+
|
| 34 |
+
def get_hf_ip_via_google():
|
| 35 |
+
global resolved_hf_ip
|
| 36 |
+
if resolved_hf_ip:
|
| 37 |
+
return resolved_hf_ip
|
| 38 |
+
try:
|
| 39 |
+
req = urllib.request.Request(f"https://dns.google/resolve?name={HF_DOMAIN}&type=A")
|
| 40 |
+
with urllib.request.urlopen(req, timeout=5) as response:
|
| 41 |
+
data = json.loads(response.read().decode('utf-8'))
|
| 42 |
+
for answer in data.get("Answer", []):
|
| 43 |
+
if answer.get("type") == 1:
|
| 44 |
+
resolved_hf_ip = answer.get("data")
|
| 45 |
+
print(f"[DNS] Got HF IP via Google: {resolved_hf_ip}")
|
| 46 |
+
return resolved_hf_ip
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"[DNS] Google DoH failed: {e}")
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
original_getaddrinfo = socket.getaddrinfo
|
| 52 |
+
|
| 53 |
+
def custom_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
| 54 |
+
if host == HF_DOMAIN:
|
| 55 |
+
ip = get_hf_ip_via_google()
|
| 56 |
+
if ip:
|
| 57 |
+
return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, '', (ip, port))]
|
| 58 |
+
return original_getaddrinfo(host, port, family, type, proto, flags)
|
| 59 |
+
|
| 60 |
+
socket.getaddrinfo = custom_getaddrinfo
|
| 61 |
+
|
| 62 |
+
# === FOLDERS ===
|
| 63 |
+
def ensure_folders():
|
| 64 |
+
for folder in ["skills", "projects", "downloads", "prompts", "build_modes"]:
|
| 65 |
+
os.makedirs(folder, exist_ok=True)
|
file_manager.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Работа с файлами"""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import fnmatch
|
| 5 |
+
from typing import Dict, List, Tuple, Any
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
class FileManager:
|
| 9 |
+
SUPPORTED_EXTENSIONS = {
|
| 10 |
+
'.txt': 'text', '.py': 'python', '.js': 'javascript',
|
| 11 |
+
'.html': 'html', '.css': 'css', '.json': 'json',
|
| 12 |
+
'.yaml': 'yaml', '.yml': 'yaml', '.md': 'markdown',
|
| 13 |
+
'.csv': 'csv', '.xml': 'xml', '.log': 'log',
|
| 14 |
+
'.sql': 'sql', '.sh': 'bash', '.bat': 'batch',
|
| 15 |
+
'.ps1': 'powershell', '.ipynb': 'jupyter',
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
def __init__(self, base_dir: str = "."):
|
| 19 |
+
self.base_dir = base_dir
|
| 20 |
+
|
| 21 |
+
def read_file(self, file_path: str, max_size: int = 100000) -> Tuple[str, str]:
|
| 22 |
+
full_path = os.path.join(self.base_dir, file_path)
|
| 23 |
+
if not os.path.exists(full_path):
|
| 24 |
+
return ("", f"❌ Файл не найден: {file_path}")
|
| 25 |
+
try:
|
| 26 |
+
size = os.path.getsize(full_path)
|
| 27 |
+
if size > max_size:
|
| 28 |
+
return ("", f"⚠️ Файл слишком большой ({size} bytes > {max_size})")
|
| 29 |
+
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
|
| 30 |
+
content = f.read()
|
| 31 |
+
ext = os.path.splitext(file_path)[1].lower()
|
| 32 |
+
lang = self.SUPPORTED_EXTENSIONS.get(ext, 'text')
|
| 33 |
+
return (content, f"✅ Прочитано {len(content)} chars ({lang})")
|
| 34 |
+
except Exception as e:
|
| 35 |
+
return ("", f"❌ Ошибка чтения: {e}")
|
| 36 |
+
|
| 37 |
+
def read_multiple_files(self, file_paths: List[str], max_total: int = 50000) -> Dict[str, Tuple[str, str]]:
|
| 38 |
+
results = {}
|
| 39 |
+
total = 0
|
| 40 |
+
for fp in file_paths:
|
| 41 |
+
content, status = self.read_file(fp)
|
| 42 |
+
if content:
|
| 43 |
+
total += len(content)
|
| 44 |
+
if total > max_total:
|
| 45 |
+
results[fp] = ("", "⚠️ Превышен общий лимит")
|
| 46 |
+
break
|
| 47 |
+
results[fp] = (content, status)
|
| 48 |
+
return results
|
| 49 |
+
|
| 50 |
+
def list_files(self, directory: str = ".", pattern: str = "*", recursive: bool = False) -> List[str]:
|
| 51 |
+
base = os.path.join(self.base_dir, directory)
|
| 52 |
+
if not os.path.exists(base):
|
| 53 |
+
return []
|
| 54 |
+
results = []
|
| 55 |
+
if recursive:
|
| 56 |
+
for root, dirs, files in os.walk(base):
|
| 57 |
+
for f in files:
|
| 58 |
+
rel = os.path.relpath(os.path.join(root, f), self.base_dir)
|
| 59 |
+
if self._match_pattern(f, pattern):
|
| 60 |
+
results.append(rel)
|
| 61 |
+
else:
|
| 62 |
+
for f in os.listdir(base):
|
| 63 |
+
if os.path.isfile(os.path.join(base, f)) and self._match_pattern(f, pattern):
|
| 64 |
+
results.append(os.path.join(directory, f))
|
| 65 |
+
return results
|
| 66 |
+
|
| 67 |
+
def save_file(self, file_path: str, content: str) -> str:
|
| 68 |
+
full_path = os.path.join(self.base_dir, file_path)
|
| 69 |
+
try:
|
| 70 |
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
| 71 |
+
with open(full_path, 'w', encoding='utf-8') as f:
|
| 72 |
+
f.write(content)
|
| 73 |
+
return f"✅ Сохранено: {file_path} ({len(content)} chars)"
|
| 74 |
+
except Exception as e:
|
| 75 |
+
return f"❌ Ошибка сохранения: {e}"
|
| 76 |
+
|
| 77 |
+
def analyze_file(self, file_path: str) -> Dict[str, Any]:
|
| 78 |
+
full_path = os.path.join(self.base_dir, file_path)
|
| 79 |
+
if not os.path.exists(full_path):
|
| 80 |
+
return {"error": "Файл не найден"}
|
| 81 |
+
try:
|
| 82 |
+
stat = os.stat(full_path)
|
| 83 |
+
ext = os.path.splitext(file_path)[1].lower()
|
| 84 |
+
lang = self.SUPPORTED_EXTENSIONS.get(ext, 'unknown')
|
| 85 |
+
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
|
| 86 |
+
content = f.read()
|
| 87 |
+
lines = content.count('\n') + 1
|
| 88 |
+
return {
|
| 89 |
+
"path": file_path,
|
| 90 |
+
"size": stat.st_size,
|
| 91 |
+
"lines": lines,
|
| 92 |
+
"language": lang,
|
| 93 |
+
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
| 94 |
+
"chars": len(content)
|
| 95 |
+
}
|
| 96 |
+
except Exception as e:
|
| 97 |
+
return {"error": str(e)}
|
| 98 |
+
|
| 99 |
+
def _match_pattern(self, filename: str, pattern: str) -> bool:
|
| 100 |
+
if pattern == "*":
|
| 101 |
+
return True
|
| 102 |
+
return fnmatch.fnmatch(filename, pattern)
|
| 103 |
+
|
| 104 |
+
FILE_MANAGER = FileManager()
|
helpers.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Вспомогательные функции"""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Dict, List, Tuple, Any
|
| 5 |
+
from .state import STATE
|
| 6 |
+
|
| 7 |
+
def build_skill_prompt(new_request: str, file_context: str = "") -> str:
|
| 8 |
+
history = STATE.skill_history[-5:] if STATE.skill_history else []
|
| 9 |
+
context = ""
|
| 10 |
+
for entry in history:
|
| 11 |
+
context += f"\n{entry['role']}: {entry['content'][:500]}"
|
| 12 |
+
prompt = f"Previous context:{context}\n\nNew request: {new_request}"
|
| 13 |
+
if file_context:
|
| 14 |
+
prompt += f"\n\nFile context:\n{file_context}"
|
| 15 |
+
return prompt
|
| 16 |
+
|
| 17 |
+
def parse_build_args(text: str) -> Tuple[Dict[str, Any], str]:
|
| 18 |
+
params = {}
|
| 19 |
+
# --agents=N
|
| 20 |
+
agents_match = re.search(r'--agents=(\d+)', text)
|
| 21 |
+
if agents_match:
|
| 22 |
+
params["agents"] = int(agents_match.group(1))
|
| 23 |
+
text = text.replace(agents_match.group(0), "")
|
| 24 |
+
# --tier=tier1|tier2|...
|
| 25 |
+
tier_match = re.search(r'--tier=(\w+)', text)
|
| 26 |
+
if tier_match:
|
| 27 |
+
params["tier"] = tier_match.group(1)
|
| 28 |
+
text = text.replace(tier_match.group(0), "")
|
| 29 |
+
# --interpreter=true|false
|
| 30 |
+
interp_match = re.search(r'--interpreter=(true|false)', text, re.IGNORECASE)
|
| 31 |
+
if interp_match:
|
| 32 |
+
params["use_interpreter"] = interp_match.group(1).lower() == "true"
|
| 33 |
+
text = text.replace(interp_match.group(0), "")
|
| 34 |
+
return params, text.strip()
|
| 35 |
+
|
| 36 |
+
def parse_skill_args(text: str) -> Tuple[Dict[str, Any], str]:
|
| 37 |
+
params = {}
|
| 38 |
+
# --agents=role1,role2
|
| 39 |
+
agents_match = re.search(r'--agents=([\w,]+)', text)
|
| 40 |
+
if agents_match:
|
| 41 |
+
params["agents"] = agents_match.group(1).split(",")
|
| 42 |
+
text = text.replace(agents_match.group(0), "")
|
| 43 |
+
# --internet
|
| 44 |
+
if "--internet" in text:
|
| 45 |
+
params["internet"] = True
|
| 46 |
+
text = text.replace("--internet", "")
|
| 47 |
+
return params, text.strip()
|
| 48 |
+
|
| 49 |
+
def parse_skill_agents(text: str) -> Tuple[List[str], str]:
|
| 50 |
+
params, clean = parse_skill_args(text)
|
| 51 |
+
return params.get("agents", []), clean
|
| 52 |
+
|
| 53 |
+
def get_current_mode_info() -> str:
|
| 54 |
+
mode = STATE.current_mode
|
| 55 |
+
role = STATE.current_role
|
| 56 |
+
model = STATE.current_model
|
| 57 |
+
conductor = STATE.current_conductor
|
| 58 |
+
return f"🎛️ Режим: {mode} | Роль: {role} | Модель: {model} | Кондуктор: {conductor}"
|
internet_agent.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Бесплатный интернет-агент с минимум 3 способами поиска"""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import requests
|
| 5 |
+
import hashlib
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from typing import Dict, List, Tuple, Any, Optional
|
| 8 |
+
from collections import Counter
|
| 9 |
+
|
| 10 |
+
class FreeInternetAgent:
|
| 11 |
+
"""Интернет-агент с бесплатными поисковыми системами (минимум 3 способа)"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, cache_ttl: int = 3600):
|
| 14 |
+
self.session = requests.Session()
|
| 15 |
+
self.session.headers.update({
|
| 16 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
| 17 |
+
})
|
| 18 |
+
self.cache: Dict[str, Tuple[Any, datetime]] = {}
|
| 19 |
+
self.cache_ttl = cache_ttl
|
| 20 |
+
self._has_bs4 = False
|
| 21 |
+
try:
|
| 22 |
+
import bs4
|
| 23 |
+
self._has_bs4 = True
|
| 24 |
+
except ImportError:
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
# SearXNG инстансы
|
| 28 |
+
self.searxng_instances = [
|
| 29 |
+
"https://searx.be",
|
| 30 |
+
"https://search.bus-hit.me",
|
| 31 |
+
"https://searx.nixnet.xyz",
|
| 32 |
+
"https://searx.tuxcloud.net",
|
| 33 |
+
"https://searx.moe",
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
def _get_cache_key(self, *args, **kwargs) -> str:
|
| 37 |
+
key = f"{args}_{sorted(kwargs.items())}"
|
| 38 |
+
return hashlib.md5(key.encode()).hexdigest()
|
| 39 |
+
|
| 40 |
+
def _get_from_cache(self, key: str) -> Optional[Any]:
|
| 41 |
+
if key in self.cache:
|
| 42 |
+
data, timestamp = self.cache[key]
|
| 43 |
+
if datetime.now() - timestamp < timedelta(seconds=self.cache_ttl):
|
| 44 |
+
return data
|
| 45 |
+
else:
|
| 46 |
+
del self.cache[key]
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
def _save_to_cache(self, key: str, data: Any) -> None:
|
| 50 |
+
self.cache[key] = (data, datetime.now())
|
| 51 |
+
|
| 52 |
+
def search_web(self, query: str, num_results: int = 5) -> List[Dict[str, str]]:
|
| 53 |
+
"""Умный поиск с несколькими источниками (минимум 3 способа)"""
|
| 54 |
+
cache_key = self._get_cache_key('search', query, num_results)
|
| 55 |
+
cached = self._get_from_cache(cache_key)
|
| 56 |
+
if cached is not None:
|
| 57 |
+
return cached
|
| 58 |
+
|
| 59 |
+
results = []
|
| 60 |
+
|
| 61 |
+
# Способ 1: SearXNG (мета-поиск)
|
| 62 |
+
results = self._search_searxng(query, num_results)
|
| 63 |
+
|
| 64 |
+
# Способ 2: DuckDuckGo API
|
| 65 |
+
if not results:
|
| 66 |
+
results = self._search_duckduckgo(query, num_results)
|
| 67 |
+
|
| 68 |
+
# Способ 3: Google (парсинг)
|
| 69 |
+
if not results:
|
| 70 |
+
results = self._search_google(query, num_results)
|
| 71 |
+
|
| 72 |
+
# Способ 4: Яндекс (для русского) — опционально
|
| 73 |
+
if not results and any(ord(c) > 1024 for c in query):
|
| 74 |
+
results = self._search_yandex(query, num_results)
|
| 75 |
+
|
| 76 |
+
self._save_to_cache(cache_key, results)
|
| 77 |
+
return results
|
| 78 |
+
|
| 79 |
+
def _search_searxng(self, query: str, num_results: int) -> List[Dict[str, str]]:
|
| 80 |
+
results = []
|
| 81 |
+
for instance in self.searxng_instances:
|
| 82 |
+
try:
|
| 83 |
+
url = f"{instance}/search"
|
| 84 |
+
params = {
|
| 85 |
+
"q": query,
|
| 86 |
+
"format": "json",
|
| 87 |
+
"categories": "general",
|
| 88 |
+
"engines": "google,bing,duckduckgo,startpage",
|
| 89 |
+
"language": "en",
|
| 90 |
+
"pageno": 1
|
| 91 |
+
}
|
| 92 |
+
response = self.session.get(url, params=params, timeout=20)
|
| 93 |
+
response.raise_for_status()
|
| 94 |
+
data = response.json()
|
| 95 |
+
|
| 96 |
+
if 'results' in data:
|
| 97 |
+
for item in data['results'][:num_results]:
|
| 98 |
+
results.append({
|
| 99 |
+
'title': item.get('title', '')[:100],
|
| 100 |
+
'url': item.get('url', ''),
|
| 101 |
+
'snippet': item.get('content', '')[:200],
|
| 102 |
+
'source': 'searxng',
|
| 103 |
+
'engine': item.get('engine', '')
|
| 104 |
+
})
|
| 105 |
+
if results:
|
| 106 |
+
print(f"🔍 SearXNG: {len(results)} результатов")
|
| 107 |
+
break
|
| 108 |
+
except Exception as e:
|
| 109 |
+
continue
|
| 110 |
+
return results
|
| 111 |
+
|
| 112 |
+
def _search_duckduckgo(self, query: str, num_results: int) -> List[Dict[str, str]]:
|
| 113 |
+
results = []
|
| 114 |
+
try:
|
| 115 |
+
url = f"https://api.duckduckgo.com/?q={query}&format=json&no_html=1&skip_disambig=1"
|
| 116 |
+
response = self.session.get(url, timeout=15)
|
| 117 |
+
data = response.json()
|
| 118 |
+
|
| 119 |
+
if 'RelatedTopics' in data:
|
| 120 |
+
for item in data['RelatedTopics'][:num_results]:
|
| 121 |
+
if 'Text' in item and 'FirstURL' in item:
|
| 122 |
+
results.append({
|
| 123 |
+
'title': item['Text'][:100],
|
| 124 |
+
'url': item['FirstURL'],
|
| 125 |
+
'snippet': item.get('Text', '')[:200],
|
| 126 |
+
'source': 'duckduckgo'
|
| 127 |
+
})
|
| 128 |
+
print(f"🦆 DuckDuckGo: {len(results)} результатов")
|
| 129 |
+
except Exception as e:
|
| 130 |
+
print(f"⚠️ DuckDuckGo ошибка: {e}")
|
| 131 |
+
return results
|
| 132 |
+
|
| 133 |
+
def _search_google(self, query: str, num_results: int) -> List[Dict[str, str]]:
|
| 134 |
+
results = []
|
| 135 |
+
if not self._has_bs4:
|
| 136 |
+
return results
|
| 137 |
+
try:
|
| 138 |
+
from bs4 import BeautifulSoup
|
| 139 |
+
url = f"https://www.google.com/search?q={query}&num={num_results * 2}"
|
| 140 |
+
response = self.session.get(url, timeout=20)
|
| 141 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
| 142 |
+
|
| 143 |
+
for g in soup.find_all('div', class_='g'):
|
| 144 |
+
title_elem = g.find('h3')
|
| 145 |
+
link_elem = g.find('a')
|
| 146 |
+
snippet_elem = g.find('div', class_='VwiC3b')
|
| 147 |
+
|
| 148 |
+
if title_elem and link_elem:
|
| 149 |
+
title = title_elem.get_text()
|
| 150 |
+
link = link_elem.get('href', '')
|
| 151 |
+
snippet = snippet_elem.get_text() if snippet_elem else ''
|
| 152 |
+
if link.startswith('/url?q='):
|
| 153 |
+
link = link.split('/url?q=')[1].split('&')[0]
|
| 154 |
+
if link.startswith('http'):
|
| 155 |
+
results.append({
|
| 156 |
+
'title': title[:100],
|
| 157 |
+
'url': link,
|
| 158 |
+
'snippet': snippet[:200],
|
| 159 |
+
'source': 'google'
|
| 160 |
+
})
|
| 161 |
+
if len(results) >= num_results:
|
| 162 |
+
break
|
| 163 |
+
print(f"🔍 Google: {len(results)} результатов")
|
| 164 |
+
except Exception as e:
|
| 165 |
+
print(f"⚠️ Google ошибка: {e}")
|
| 166 |
+
return results
|
| 167 |
+
|
| 168 |
+
def _search_yandex(self, query: str, num_results: int) -> List[Dict[str, str]]:
|
| 169 |
+
results = []
|
| 170 |
+
if not self._has_bs4:
|
| 171 |
+
return results
|
| 172 |
+
try:
|
| 173 |
+
from bs4 import BeautifulSoup
|
| 174 |
+
url = f"https://yandex.ru/search/?text={query}&numdoc={num_results}"
|
| 175 |
+
response = self.session.get(url, timeout=20)
|
| 176 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
| 177 |
+
|
| 178 |
+
for item in soup.find_all('li', class_='serp-item'):
|
| 179 |
+
link_elem = item.find('a', class_='link')
|
| 180 |
+
snippet_elem = item.find('div', class_='text-container')
|
| 181 |
+
if link_elem:
|
| 182 |
+
title = link_elem.get_text()
|
| 183 |
+
link = link_elem.get('href', '')
|
| 184 |
+
snippet = snippet_elem.get_text() if snippet_elem else ''
|
| 185 |
+
if link.startswith('http'):
|
| 186 |
+
results.append({
|
| 187 |
+
'title': title[:100],
|
| 188 |
+
'url': link,
|
| 189 |
+
'snippet': snippet[:200],
|
| 190 |
+
'source': 'yandex'
|
| 191 |
+
})
|
| 192 |
+
if len(results) >= num_results:
|
| 193 |
+
break
|
| 194 |
+
print(f"🔍 Яндекс: {len(results)} результатов")
|
| 195 |
+
except Exception as e:
|
| 196 |
+
print(f"⚠️ Яндекс ошибка: {e}")
|
| 197 |
+
return results
|
| 198 |
+
|
| 199 |
+
def fetch_page(self, url: str, max_size: int = 50000) -> Dict[str, Any]:
|
| 200 |
+
try:
|
| 201 |
+
response = self.session.get(url, timeout=30)
|
| 202 |
+
response.raise_for_status()
|
| 203 |
+
content = response.text[:max_size]
|
| 204 |
+
return {
|
| 205 |
+
"url": url,
|
| 206 |
+
"status": response.status_code,
|
| 207 |
+
"content": content,
|
| 208 |
+
"length": len(content),
|
| 209 |
+
"headers": dict(response.headers)
|
| 210 |
+
}
|
| 211 |
+
except Exception as e:
|
| 212 |
+
return {"url": url, "error": str(e)}
|
| 213 |
+
|
| 214 |
+
def analyze_website(self, url: str) -> Dict[str, Any]:
|
| 215 |
+
page = self.fetch_page(url)
|
| 216 |
+
if "error" in page:
|
| 217 |
+
return page
|
| 218 |
+
content = page.get("content", "")
|
| 219 |
+
return {
|
| 220 |
+
"url": url,
|
| 221 |
+
"title": content.split("<title>")[1].split("</title>")[0] if "<title>" in content else "N/A",
|
| 222 |
+
"has_forms": "form" in content.lower(),
|
| 223 |
+
"has_scripts": "<script" in content.lower(),
|
| 224 |
+
"links_count": content.count("<a "),
|
| 225 |
+
"size": len(content)
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
def fetch_multiple(self, urls: List[str], max_total: int = 50000) -> List[Dict[str, Any]]:
|
| 229 |
+
results = []
|
| 230 |
+
total = 0
|
| 231 |
+
for url in urls:
|
| 232 |
+
page = self.fetch_page(url, max_size=min(10000, max_total - total))
|
| 233 |
+
if "content" in page:
|
| 234 |
+
total += len(page["content"])
|
| 235 |
+
results.append(page)
|
| 236 |
+
if total >= max_total:
|
| 237 |
+
break
|
| 238 |
+
return results
|
| 239 |
+
|
| 240 |
+
def clear_cache(self) -> str:
|
| 241 |
+
count = len(self.cache)
|
| 242 |
+
self.cache.clear()
|
| 243 |
+
return f"🗑️ Кэш очищен ({count} записей)"
|
| 244 |
+
|
| 245 |
+
def get_cache_stats(self) -> str:
|
| 246 |
+
return f"📊 Кэш: {len(self.cache)} записей, TTL: {self.cache_ttl} секунд"
|
| 247 |
+
|
| 248 |
+
def _extract_keywords(self, content: str) -> List[str]:
|
| 249 |
+
words = re.findall(r'\b[a-zA-Z]{4,}\b', content.lower())
|
| 250 |
+
return [w for w, _ in Counter(words).most_common(10)]
|
| 251 |
+
|
| 252 |
+
def _detect_language(self, content: str) -> str:
|
| 253 |
+
ru_chars = len(re.findall(r'[а-яА-Я]', content))
|
| 254 |
+
en_chars = len(re.findall(r'[a-zA-Z]', content))
|
| 255 |
+
if ru_chars > en_chars * 0.5:
|
| 256 |
+
return "ru"
|
| 257 |
+
return "en"
|
| 258 |
+
|
| 259 |
+
INTERNET_AGENT = FreeInternetAgent(cache_ttl=3600)
|
main.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Точка входа — запуск сервера"""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from http.server import HTTPServer
|
| 5 |
+
from .config import PORT, TOKEN, API_KEY, ensure_folders
|
| 6 |
+
from .state import STATE
|
| 7 |
+
from .internet_agent import INTERNET_AGENT
|
| 8 |
+
from .routes import WebhookHandler
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
print("=" * 60)
|
| 12 |
+
print("🧠 PINKSKY v7.0 — МОДУЛЬНАЯ АРХИТЕКТУРА")
|
| 13 |
+
print("=" * 60)
|
| 14 |
+
|
| 15 |
+
if not TOKEN:
|
| 16 |
+
print("❌ TELEGRAM_BOT_TOKEN не найден!")
|
| 17 |
+
sys.exit(1)
|
| 18 |
+
if not API_KEY:
|
| 19 |
+
print("⚠️ NVIDIA_API_KEY не найден")
|
| 20 |
+
|
| 21 |
+
ensure_folders()
|
| 22 |
+
|
| 23 |
+
print(f"📊 Загружено моделей: {len(STATE.models)}")
|
| 24 |
+
print(f"📊 Загружено ролей: {len(STATE.roles)}")
|
| 25 |
+
print(f"📊 Загружено кондукторов: {len(STATE.conductors)}")
|
| 26 |
+
print(f"🌐 Интернет: {'✅' if STATE.build_context.get('internet_access', True) else '❌'}")
|
| 27 |
+
print(f"🗑️ Кэш: {INTERNET_AGENT.get_cache_stats()}")
|
| 28 |
+
|
| 29 |
+
STATE.current_mode = "chat"
|
| 30 |
+
server = HTTPServer(("0.0.0.0", PORT), WebhookHandler)
|
| 31 |
+
print(f"🚀 Сервер запущен на порту {PORT}")
|
| 32 |
+
print("=" * 60)
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
server.serve_forever()
|
| 36 |
+
except KeyboardInterrupt:
|
| 37 |
+
print("\n🛑 Остановка...")
|
| 38 |
+
from .process_manager import PROCESS_MANAGER
|
| 39 |
+
PROCESS_MANAGER.cancel_all()
|
| 40 |
+
STATE.save_history()
|
| 41 |
+
print("💾 История сохранена.")
|
| 42 |
+
server.shutdown()
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
main()
|
mode_handlers.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Обработчики режимов: Chat, Skill, Build"""
|
| 2 |
+
|
| 3 |
+
import threading
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from typing import Dict, Any
|
| 6 |
+
from .state import STATE
|
| 7 |
+
from .config import API_KEY, API_BASE
|
| 8 |
+
from .universal_agent import UniversalAgent
|
| 9 |
+
from .conductor_engine import ConductorEngine
|
| 10 |
+
from .skill_orchestrator import SkillOrchestrator
|
| 11 |
+
from .process_manager import PROCESS_MANAGER
|
| 12 |
+
from .notification_system import NOTIFICATIONS
|
| 13 |
+
from .helpers import parse_skill_args, build_skill_prompt, parse_build_args
|
| 14 |
+
from .telegram_utils import send_tg, send_tg_file
|
| 15 |
+
|
| 16 |
+
def configure_interpreter_for_model(model_name: str):
|
| 17 |
+
try:
|
| 18 |
+
from interpreter import interpreter
|
| 19 |
+
model = STATE.models.get(model_name, STATE.models["deepseek-v4-pro"])
|
| 20 |
+
interpreter.llm.model = f"openai/{model.endpoint}"
|
| 21 |
+
interpreter.llm.api_key = API_KEY
|
| 22 |
+
interpreter.llm.api_base = API_BASE
|
| 23 |
+
interpreter.llm.context_window = model.context_window
|
| 24 |
+
interpreter.llm.max_tokens = model.max_tokens
|
| 25 |
+
except ImportError:
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
def run_chat_mode(chat_id: str, text: str, file_context: str = "") -> str:
|
| 29 |
+
def chat_task():
|
| 30 |
+
STATE.cancel_flag = False
|
| 31 |
+
STATE.current_mode = "chat"
|
| 32 |
+
send_tg(chat_id, "🧠 Conductor анализирует запрос...")
|
| 33 |
+
try:
|
| 34 |
+
conductor_engine = ConductorEngine(STATE)
|
| 35 |
+
result = conductor_engine.orchestrate(text + file_context, chat_id=chat_id)
|
| 36 |
+
if not STATE.cancel_flag:
|
| 37 |
+
send_tg(chat_id, result)
|
| 38 |
+
STATE.add_to_history("chat", "assistant", result)
|
| 39 |
+
except Exception as e:
|
| 40 |
+
if not STATE.cancel_flag:
|
| 41 |
+
send_tg(chat_id, f"❌ Ошибка: {e}")
|
| 42 |
+
|
| 43 |
+
t = threading.Thread(target=chat_task)
|
| 44 |
+
PROCESS_MANAGER.register_thread(t)
|
| 45 |
+
t.start()
|
| 46 |
+
return "Chat mode started"
|
| 47 |
+
|
| 48 |
+
def run_skill_mode(chat_id: str, text: str, file_context: str = "") -> str:
|
| 49 |
+
params, clean_text = parse_skill_args(text)
|
| 50 |
+
agents = params.get("agents", [])
|
| 51 |
+
use_internet = params.get("internet", False)
|
| 52 |
+
|
| 53 |
+
def skill_task():
|
| 54 |
+
STATE.cancel_flag = False
|
| 55 |
+
STATE.current_mode = "skill"
|
| 56 |
+
send_tg(chat_id, "🔧 Skill Mode: запуск агентов...")
|
| 57 |
+
|
| 58 |
+
if use_internet:
|
| 59 |
+
search_results = INTERNET_AGENT.search_web(clean_text[:200])
|
| 60 |
+
if search_results:
|
| 61 |
+
web_context = "\n\nИнтернет-результаты:\n" + "\n".join(
|
| 62 |
+
f"- {r['title']}: {r['snippet']}" for r in search_results[:3]
|
| 63 |
+
)
|
| 64 |
+
file_context += web_context
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
orchestrator = SkillOrchestrator(STATE)
|
| 68 |
+
result = orchestrator.execute_with_agents(chat_id, clean_text, file_context, agents)
|
| 69 |
+
if not STATE.cancel_flag:
|
| 70 |
+
send_tg(chat_id, result)
|
| 71 |
+
STATE.add_to_history("skill", "assistant", result)
|
| 72 |
+
except Exception as e:
|
| 73 |
+
if not STATE.cancel_flag:
|
| 74 |
+
send_tg(chat_id, f"❌ Skill error: {e}")
|
| 75 |
+
|
| 76 |
+
t = threading.Thread(target=skill_task)
|
| 77 |
+
PROCESS_MANAGER.register_thread(t)
|
| 78 |
+
t.start()
|
| 79 |
+
return "Skill mode started"
|
| 80 |
+
|
| 81 |
+
def run_build_mode(chat_id: str, text: str, file_context: str = "", params: Dict[str, Any] = None) -> str:
|
| 82 |
+
def build_task():
|
| 83 |
+
STATE.cancel_flag = False
|
| 84 |
+
STATE.current_mode = "build"
|
| 85 |
+
send_tg(chat_id, "🏗️ Build Mode: запуск оркестрации...")
|
| 86 |
+
try:
|
| 87 |
+
conductor_engine = ConductorEngine(STATE)
|
| 88 |
+
if params:
|
| 89 |
+
STATE.build_context.update(params)
|
| 90 |
+
result = conductor_engine.orchestrate(text + file_context, chat_id=chat_id, build_params=params)
|
| 91 |
+
if not STATE.cancel_flag:
|
| 92 |
+
send_tg(chat_id, result)
|
| 93 |
+
STATE.add_to_history("build", "assistant", result)
|
| 94 |
+
except Exception as e:
|
| 95 |
+
if not STATE.cancel_flag:
|
| 96 |
+
send_tg(chat_id, f"❌ Build error: {e}")
|
| 97 |
+
|
| 98 |
+
t = threading.Thread(target=build_task)
|
| 99 |
+
PROCESS_MANAGER.register_thread(t)
|
| 100 |
+
t.start()
|
| 101 |
+
return "Build mode started"
|
model_ranking.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Все модели NVIDIA с ранжированием"""
|
| 2 |
+
|
| 3 |
+
from .config import HF_FALLBACK_MODEL
|
| 4 |
+
|
| 5 |
+
MODEL_RANKING = {
|
| 6 |
+
# TIER 1: ELITE CODING
|
| 7 |
+
"deepseek-v4-pro": {
|
| 8 |
+
"endpoint": "deepseek-ai/deepseek-v4-pro",
|
| 9 |
+
"context_window": 64000, "max_tokens": 8000,
|
| 10 |
+
"coding_rank": 1, "speed_rank": 15, "reasoning_rank": 1,
|
| 11 |
+
"cost_per_1k_input": 0.001, "cost_per_1k_output": 0.005,
|
| 12 |
+
"tags": ["elite", "coding", "reasoning", "math", "cheap"]
|
| 13 |
+
},
|
| 14 |
+
"kimi-k2.6": {
|
| 15 |
+
"endpoint": "moonshotai/kimi-k2.6",
|
| 16 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 17 |
+
"coding_rank": 2, "speed_rank": 12, "reasoning_rank": 2,
|
| 18 |
+
"cost_per_1k_input": 0.003, "cost_per_1k_output": 0.015,
|
| 19 |
+
"tags": ["elite", "coding", "reasoning", "long_context"]
|
| 20 |
+
},
|
| 21 |
+
"qwen3.5-397b": {
|
| 22 |
+
"endpoint": "qwen/qwen3.5-397b-a17b",
|
| 23 |
+
"context_window": 128000, "max_tokens": 8000,
|
| 24 |
+
"coding_rank": 3, "speed_rank": 18, "reasoning_rank": 3,
|
| 25 |
+
"cost_per_1k_input": 0.002, "cost_per_1k_output": 0.008,
|
| 26 |
+
"tags": ["elite", "coding", "long_context", "chinese"]
|
| 27 |
+
},
|
| 28 |
+
"mistral-large-3": {
|
| 29 |
+
"endpoint": "mistralai/mistral-large-3-675b-instruct-2512",
|
| 30 |
+
"context_window": 128000, "max_tokens": 8000,
|
| 31 |
+
"coding_rank": 4, "speed_rank": 14, "reasoning_rank": 4,
|
| 32 |
+
"cost_per_1k_input": 0.002, "cost_per_1k_output": 0.010,
|
| 33 |
+
"tags": ["elite", "coding", "multilingual", "long_context"]
|
| 34 |
+
},
|
| 35 |
+
"gpt-oss-120b": {
|
| 36 |
+
"endpoint": "openai/gpt-oss-120b",
|
| 37 |
+
"context_window": 128000, "max_tokens": 8000,
|
| 38 |
+
"coding_rank": 5, "speed_rank": 20, "reasoning_rank": 5,
|
| 39 |
+
"cost_per_1k_input": 0.003, "cost_per_1k_output": 0.012,
|
| 40 |
+
"tags": ["elite", "coding", "reasoning", "openai"]
|
| 41 |
+
},
|
| 42 |
+
|
| 43 |
+
# TIER 2: STRONG CODING
|
| 44 |
+
"deepseek-v4-flash": {
|
| 45 |
+
"endpoint": "deepseek-ai/deepseek-v4-flash",
|
| 46 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 47 |
+
"coding_rank": 6, "speed_rank": 8, "reasoning_rank": 8,
|
| 48 |
+
"cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002,
|
| 49 |
+
"tags": ["strong", "coding", "fast", "cheap"]
|
| 50 |
+
},
|
| 51 |
+
"llama-4-maverick": {
|
| 52 |
+
"endpoint": "meta/llama-4-maverick-17b-128e-instruct",
|
| 53 |
+
"context_window": 128000, "max_tokens": 8000,
|
| 54 |
+
"coding_rank": 7, "speed_rank": 10, "reasoning_rank": 7,
|
| 55 |
+
"cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004,
|
| 56 |
+
"tags": ["strong", "coding", "meta", "long_context"]
|
| 57 |
+
},
|
| 58 |
+
"nemotron-3-super": {
|
| 59 |
+
"endpoint": "nvidia/nemotron-3-super-120b-a12b",
|
| 60 |
+
"context_window": 128000, "max_tokens": 8000,
|
| 61 |
+
"coding_rank": 8, "speed_rank": 16, "reasoning_rank": 6,
|
| 62 |
+
"cost_per_1k_input": 0.002, "cost_per_1k_output": 0.008,
|
| 63 |
+
"tags": ["strong", "coding", "nvidia", "reasoning"]
|
| 64 |
+
},
|
| 65 |
+
"mistral-medium-3.5": {
|
| 66 |
+
"endpoint": "mistralai/mistral-medium-3.5-128b",
|
| 67 |
+
"context_window": 64000, "max_tokens": 8000,
|
| 68 |
+
"coding_rank": 9, "speed_rank": 11, "reasoning_rank": 10,
|
| 69 |
+
"cost_per_1k_input": 0.001, "cost_per_1k_output": 0.005,
|
| 70 |
+
"tags": ["strong", "coding", "mistral", "balanced"]
|
| 71 |
+
},
|
| 72 |
+
"dracarys-llama-70b": {
|
| 73 |
+
"endpoint": "abacusai/dracarys-llama-3.1-70b-instruct",
|
| 74 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 75 |
+
"coding_rank": 10, "speed_rank": 13, "reasoning_rank": 11,
|
| 76 |
+
"cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004,
|
| 77 |
+
"tags": ["strong", "coding", "roleplay", "creative"]
|
| 78 |
+
},
|
| 79 |
+
"llama-3.3-70b": {
|
| 80 |
+
"endpoint": "meta/llama-3.3-70b-instruct",
|
| 81 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 82 |
+
"coding_rank": 11, "speed_rank": 9, "reasoning_rank": 12,
|
| 83 |
+
"cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002,
|
| 84 |
+
"tags": ["strong", "coding", "meta", "fast", "cheap"]
|
| 85 |
+
},
|
| 86 |
+
"nemotron-super-49b": {
|
| 87 |
+
"endpoint": "nvidia/llama-3.3-nemotron-super-49b-v1.5",
|
| 88 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 89 |
+
"coding_rank": 12, "speed_rank": 7, "reasoning_rank": 13,
|
| 90 |
+
"cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002,
|
| 91 |
+
"tags": ["strong", "coding", "nvidia", "fast", "cheap"]
|
| 92 |
+
},
|
| 93 |
+
|
| 94 |
+
# TIER 3: GOOD CODING
|
| 95 |
+
"step-3.7-flash": {
|
| 96 |
+
"endpoint": "stepfun-ai/step-3.7-flash",
|
| 97 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 98 |
+
"coding_rank": 13, "speed_rank": 6, "reasoning_rank": 14,
|
| 99 |
+
"cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002,
|
| 100 |
+
"tags": ["good", "coding", "fast", "chinese", "cheap"]
|
| 101 |
+
},
|
| 102 |
+
"mistral-small-4": {
|
| 103 |
+
"endpoint": "mistralai/mistral-small-4-119b-2603",
|
| 104 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 105 |
+
"coding_rank": 14, "speed_rank": 5, "reasoning_rank": 15,
|
| 106 |
+
"cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002,
|
| 107 |
+
"tags": ["good", "coding", "fast", "mistral", "cheap"]
|
| 108 |
+
},
|
| 109 |
+
"minimax-m2.7": {
|
| 110 |
+
"endpoint": "minimaxai/minimax-m2.7",
|
| 111 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 112 |
+
"coding_rank": 15, "speed_rank": 4, "reasoning_rank": 16,
|
| 113 |
+
"cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002,
|
| 114 |
+
"tags": ["good", "coding", "fast", "chinese", "cheap"]
|
| 115 |
+
},
|
| 116 |
+
"nemotron-super-49b-v1": {
|
| 117 |
+
"endpoint": "nvidia/llama-3.3-nemotron-super-49b-v1",
|
| 118 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 119 |
+
"coding_rank": 16, "speed_rank": 17, "reasoning_rank": 17,
|
| 120 |
+
"cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002,
|
| 121 |
+
"tags": ["good", "coding", "nvidia", "cheap"]
|
| 122 |
+
},
|
| 123 |
+
"llama-3.2-90b-vision": {
|
| 124 |
+
"endpoint": "meta/llama-3.2-90b-vision-instruct",
|
| 125 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 126 |
+
"coding_rank": 17, "speed_rank": 19, "reasoning_rank": 18,
|
| 127 |
+
"cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004,
|
| 128 |
+
"tags": ["good", "coding", "vision", "multimodal", "meta"]
|
| 129 |
+
},
|
| 130 |
+
|
| 131 |
+
# TIER 4: FAST / LIGHT
|
| 132 |
+
"nemotron-nano-12b": {
|
| 133 |
+
"endpoint": "nvidia/nemotron-nano-12b-v2-vl",
|
| 134 |
+
"context_window": 16000, "max_tokens": 4000,
|
| 135 |
+
"coding_rank": 18, "speed_rank": 2, "reasoning_rank": 22,
|
| 136 |
+
"cost_per_1k_input": 0.0001, "cost_per_1k_output": 0.0005,
|
| 137 |
+
"tags": ["light", "fast", "vision", "nvidia", "cheap"]
|
| 138 |
+
},
|
| 139 |
+
"nemotron-3-nano-30b": {
|
| 140 |
+
"endpoint": "nvidia/nemotron-3-nano-30b-a3b",
|
| 141 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 142 |
+
"coding_rank": 19, "speed_rank": 3, "reasoning_rank": 19,
|
| 143 |
+
"cost_per_1k_input": 0.0002, "cost_per_1k_output": 0.001,
|
| 144 |
+
"tags": ["light", "fast", "nvidia", "cheap"]
|
| 145 |
+
},
|
| 146 |
+
"nemotron-nano-9b": {
|
| 147 |
+
"endpoint": "nvidia/nvidia-nemotron-nano-9b-v2",
|
| 148 |
+
"context_window": 16000, "max_tokens": 4000,
|
| 149 |
+
"coding_rank": 20, "speed_rank": 1, "reasoning_rank": 23,
|
| 150 |
+
"cost_per_1k_input": 0.0001, "cost_per_1k_output": 0.0005,
|
| 151 |
+
"tags": ["light", "fastest", "nvidia", "cheap"]
|
| 152 |
+
},
|
| 153 |
+
"nemotron-content-safety": {
|
| 154 |
+
"endpoint": "nvidia/nemotron-content-safety-reasoning-4b",
|
| 155 |
+
"context_window": 8000, "max_tokens": 2000,
|
| 156 |
+
"coding_rank": 21, "speed_rank": 1, "reasoning_rank": 24,
|
| 157 |
+
"cost_per_1k_input": 0.0001, "cost_per_1k_output": 0.0005,
|
| 158 |
+
"tags": ["light", "fastest", "safety", "nvidia", "cheap"]
|
| 159 |
+
},
|
| 160 |
+
|
| 161 |
+
# TIER 5: SPECIALIZED
|
| 162 |
+
"nemotron-3-nano-omni": {
|
| 163 |
+
"endpoint": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
|
| 164 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 165 |
+
"coding_rank": 22, "speed_rank": 5, "reasoning_rank": 20,
|
| 166 |
+
"cost_per_1k_input": 0.0002, "cost_per_1k_output": 0.001,
|
| 167 |
+
"tags": ["specialized", "omni", "multimodal", "reasoning", "nvidia", "cheap"]
|
| 168 |
+
},
|
| 169 |
+
"diffusiongemma": {
|
| 170 |
+
"endpoint": "google/diffusiongemma-26b-a4b-it",
|
| 171 |
+
"context_window": 16000, "max_tokens": 4000,
|
| 172 |
+
"coding_rank": 23, "speed_rank": 10, "reasoning_rank": 25,
|
| 173 |
+
"cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004,
|
| 174 |
+
"tags": ["specialized", "image", "diffusion", "google"]
|
| 175 |
+
},
|
| 176 |
+
|
| 177 |
+
# LEGACY
|
| 178 |
+
"glm": {
|
| 179 |
+
"endpoint": "z-ai/glm-5.1",
|
| 180 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 181 |
+
"coding_rank": 9, "speed_rank": 8, "reasoning_rank": 9,
|
| 182 |
+
"cost_per_1k_input": 0.002, "cost_per_1k_output": 0.008,
|
| 183 |
+
"tags": ["legacy", "coding", "fast", "chinese"]
|
| 184 |
+
},
|
| 185 |
+
"hf_fallback": {
|
| 186 |
+
"endpoint": HF_FALLBACK_MODEL,
|
| 187 |
+
"context_window": 32000, "max_tokens": 8000,
|
| 188 |
+
"coding_rank": 50, "speed_rank": 50, "reasoning_rank": 50,
|
| 189 |
+
"cost_per_1k_input": 0, "cost_per_1k_output": 0,
|
| 190 |
+
"tags": ["fallback", "free", "hf"]
|
| 191 |
+
},
|
| 192 |
+
}
|
models.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Датаклассы для моделей, ролей и кондукторов"""
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class ModelConfig:
|
| 8 |
+
name: str
|
| 9 |
+
provider: str
|
| 10 |
+
endpoint: str
|
| 11 |
+
api_key_env: str
|
| 12 |
+
context_window: int = 32000
|
| 13 |
+
max_tokens: int = 8000
|
| 14 |
+
cost_per_1k_input: float = 0.0
|
| 15 |
+
cost_per_1k_output: float = 0.0
|
| 16 |
+
coding_rank: int = 50
|
| 17 |
+
speed_rank: int = 50
|
| 18 |
+
reasoning_rank: int = 50
|
| 19 |
+
tags: List[str] = field(default_factory=list)
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class Role:
|
| 23 |
+
name: str
|
| 24 |
+
prompt: str
|
| 25 |
+
description: str
|
| 26 |
+
preferred_models: List[str] = field(default_factory=list)
|
| 27 |
+
complexity: str = "medium"
|
| 28 |
+
tags: List[str] = field(default_factory=list)
|
| 29 |
+
tools: List[str] = field(default_factory=list)
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class Conductor:
|
| 33 |
+
name: str
|
| 34 |
+
prompt: str
|
| 35 |
+
description: str
|
| 36 |
+
strategy: str = "parallel"
|
| 37 |
+
max_agents: int = 3
|
| 38 |
+
cost_aware: bool = True
|
| 39 |
+
auto_rank_by: str = "coding"
|
notification_system.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Система уведомлений"""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from typing import Dict, Any
|
| 6 |
+
|
| 7 |
+
class NotificationSystem:
|
| 8 |
+
CRITICAL_ACTIONS = [
|
| 9 |
+
'delete', 'remove', 'kill', 'stop', 'shutdown',
|
| 10 |
+
'format', 'clear', 'reset', 'purge',
|
| 11 |
+
'upload', 'publish', 'deploy', 'push',
|
| 12 |
+
'change_password', 'add_user', 'remove_user',
|
| 13 |
+
'grant_access', 'revoke_access',
|
| 14 |
+
'install', 'uninstall', 'update',
|
| 15 |
+
'execute', 'run', 'start', 'stop',
|
| 16 |
+
'create_file', 'delete_file', 'modify_file',
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
HIGH_RISK_PATTERNS = [
|
| 20 |
+
r'rm\s+-rf', r'del\s+/f', r'format\s+', r'mkfs',
|
| 21 |
+
r'drop\s+database', r'truncate\s+', r'delete\s+from',
|
| 22 |
+
r'ALTER\s+TABLE', r'DROP\s+TABLE',
|
| 23 |
+
r'chmod\s+777', r'chown\s+root',
|
| 24 |
+
r'sudo\s+', r'admin\s+',
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
def __init__(self, chat_id: str = None):
|
| 28 |
+
self.chat_id = chat_id
|
| 29 |
+
self.notification_history = []
|
| 30 |
+
self.enabled = True
|
| 31 |
+
|
| 32 |
+
def set_chat_id(self, chat_id: str):
|
| 33 |
+
self.chat_id = chat_id
|
| 34 |
+
|
| 35 |
+
def set_enabled(self, enabled: bool):
|
| 36 |
+
self.enabled = enabled
|
| 37 |
+
|
| 38 |
+
def check_action(self, action: str, context: Dict[str, Any] = None) -> bool:
|
| 39 |
+
action_lower = action.lower()
|
| 40 |
+
context = context or {}
|
| 41 |
+
for critical in self.CRITICAL_ACTIONS:
|
| 42 |
+
if critical in action_lower:
|
| 43 |
+
return True
|
| 44 |
+
for pattern in self.HIGH_RISK_PATTERNS:
|
| 45 |
+
if re.search(pattern, action_lower, re.IGNORECASE):
|
| 46 |
+
return True
|
| 47 |
+
if context.get('important', False):
|
| 48 |
+
return True
|
| 49 |
+
if context.get('files_changed', 0) > 3:
|
| 50 |
+
return True
|
| 51 |
+
return False
|
| 52 |
+
|
| 53 |
+
def notify(self, action: str, details: str = "", severity: str = "info") -> str:
|
| 54 |
+
if not self.enabled:
|
| 55 |
+
return "🔇 Уведомления отключены"
|
| 56 |
+
if not self.chat_id:
|
| 57 |
+
return "⚠️ Chat ID не установлен"
|
| 58 |
+
|
| 59 |
+
emoji_map = {'critical': '🚨', 'warning': '⚠️', 'info': 'ℹ️', 'success': '✅', 'error': '❌'}
|
| 60 |
+
emoji = emoji_map.get(severity, 'ℹ️')
|
| 61 |
+
|
| 62 |
+
message = f"{emoji} *УВЕДОМЛЕНИЕ*\n\n🔹 *Действие:* `{action}`\n"
|
| 63 |
+
if details:
|
| 64 |
+
message += f"📝 *Детали:*\n```\n{details[:500]}\n```\n"
|
| 65 |
+
message += f"🕐 *Время:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 66 |
+
|
| 67 |
+
self.notification_history.append({
|
| 68 |
+
'action': action, 'details': details,
|
| 69 |
+
'severity': severity, 'timestamp': datetime.now().isoformat()
|
| 70 |
+
})
|
| 71 |
+
|
| 72 |
+
from .telegram_utils import send_tg
|
| 73 |
+
send_tg(self.chat_id, message)
|
| 74 |
+
return f"✅ Уведомление отправлено: {action}"
|
| 75 |
+
|
| 76 |
+
def get_history(self, limit: int = 10) -> str:
|
| 77 |
+
if not self.notification_history:
|
| 78 |
+
return "📋 Нет уведомлений"
|
| 79 |
+
result = "📋 *История уведомлений:*\n\n"
|
| 80 |
+
for entry in self.notification_history[-limit:]:
|
| 81 |
+
emoji = {'critical': '🚨', 'warning': '⚠️', 'info': 'ℹ️'}.get(entry['severity'], 'ℹ️')
|
| 82 |
+
result += f"{emoji} `{entry['action']}` — {entry['timestamp']}\n"
|
| 83 |
+
return result
|
| 84 |
+
|
| 85 |
+
NOTIFICATIONS = NotificationSystem()
|
process_manager.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Управление потоками и отмена генерации"""
|
| 2 |
+
|
| 3 |
+
import threading
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 6 |
+
|
| 7 |
+
class ProcessManager:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.active_threads: List[threading.Thread] = []
|
| 10 |
+
self.cancel_flags: Dict[int, bool] = {}
|
| 11 |
+
self.lock = threading.Lock()
|
| 12 |
+
self.executor = ThreadPoolExecutor(max_workers=10)
|
| 13 |
+
self.futures = []
|
| 14 |
+
|
| 15 |
+
def register_thread(self, thread: threading.Thread) -> None:
|
| 16 |
+
with self.lock:
|
| 17 |
+
self.active_threads.append(thread)
|
| 18 |
+
self.cancel_flags[thread.ident] = False
|
| 19 |
+
|
| 20 |
+
def register_future(self, future) -> None:
|
| 21 |
+
with self.lock:
|
| 22 |
+
self.futures.append(future)
|
| 23 |
+
|
| 24 |
+
def cancel_all(self) -> str:
|
| 25 |
+
with self.lock:
|
| 26 |
+
for future in self.futures:
|
| 27 |
+
if not future.done():
|
| 28 |
+
future.cancel()
|
| 29 |
+
self.futures.clear()
|
| 30 |
+
for thread_id in self.cancel_flags:
|
| 31 |
+
self.cancel_flags[thread_id] = True
|
| 32 |
+
for thread in self.active_threads:
|
| 33 |
+
if thread.is_alive():
|
| 34 |
+
try:
|
| 35 |
+
thread.join(timeout=0.5)
|
| 36 |
+
except Exception:
|
| 37 |
+
pass
|
| 38 |
+
self.active_threads.clear()
|
| 39 |
+
self.cancel_flags.clear()
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
from interpreter import interpreter
|
| 43 |
+
if hasattr(interpreter, 'cancel'):
|
| 44 |
+
interpreter.cancel()
|
| 45 |
+
except Exception:
|
| 46 |
+
pass
|
| 47 |
+
|
| 48 |
+
from .state import STATE
|
| 49 |
+
STATE.cancel_flag = True
|
| 50 |
+
STATE.current_mode = "paused"
|
| 51 |
+
return "✅ Все процессы генерации остановлены!"
|
| 52 |
+
|
| 53 |
+
def is_cancelled(self, thread_id: int = None) -> bool:
|
| 54 |
+
if thread_id is None:
|
| 55 |
+
thread_id = threading.current_thread().ident
|
| 56 |
+
with self.lock:
|
| 57 |
+
return self.cancel_flags.get(thread_id, False)
|
| 58 |
+
|
| 59 |
+
def clear(self) -> None:
|
| 60 |
+
with self.lock:
|
| 61 |
+
self.active_threads = [t for t in self.active_threads if t.is_alive()]
|
| 62 |
+
|
| 63 |
+
def get_active_count(self) -> int:
|
| 64 |
+
with self.lock:
|
| 65 |
+
return len([t for t in self.active_threads if t.is_alive()])
|
| 66 |
+
|
| 67 |
+
PROCESS_MANAGER = ProcessManager()
|
routes.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HTTP маршруты"""
|
| 2 |
+
|
| 3 |
+
from http.server import BaseHTTPRequestHandler
|
| 4 |
+
import json
|
| 5 |
+
from .telegram_handlers import BotHandler
|
| 6 |
+
|
| 7 |
+
class WebhookHandler(BotHandler, BaseHTTPRequestHandler):
|
| 8 |
+
"""Обработчик вебхуков Telegram"""
|
| 9 |
+
|
| 10 |
+
def do_GET(self):
|
| 11 |
+
self.send_response(200)
|
| 12 |
+
self.end_headers()
|
| 13 |
+
self.wfile.write(b"PinkSky v7.0 is running!")
|
| 14 |
+
|
| 15 |
+
def do_POST(self):
|
| 16 |
+
length = int(self.headers.get('Content-Length', 0))
|
| 17 |
+
data = json.loads(self.rfile.read(length))
|
| 18 |
+
self.send_response(200)
|
| 19 |
+
self.end_headers()
|
| 20 |
+
self.wfile.write(b"OK")
|
| 21 |
+
self.handle_message(data)
|
| 22 |
+
|
| 23 |
+
def log_message(self, format, *args):
|
| 24 |
+
pass
|
skill_orchestrator.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Skill Orchestrator — многомодельный синтез в Skill Mode"""
|
| 2 |
+
|
| 3 |
+
import threading
|
| 4 |
+
from typing import Dict, List
|
| 5 |
+
from .state import STATE
|
| 6 |
+
from .universal_agent import UniversalAgent
|
| 7 |
+
from .process_manager import PROCESS_MANAGER
|
| 8 |
+
from .notification_system import NOTIFICATIONS
|
| 9 |
+
|
| 10 |
+
class SkillOrchestrator:
|
| 11 |
+
def __init__(self, state):
|
| 12 |
+
self.state = state
|
| 13 |
+
|
| 14 |
+
def execute_with_agents(self, chat_id: str, text: str, file_context: str = "", agents: List[str] = None) -> str:
|
| 15 |
+
if agents is None:
|
| 16 |
+
agents = self._auto_select_agents(text)
|
| 17 |
+
|
| 18 |
+
main_result = self._execute_main(text, file_context, chat_id)
|
| 19 |
+
if len(agents) <= 1:
|
| 20 |
+
return main_result
|
| 21 |
+
|
| 22 |
+
agent_results = self._run_agents_parallel(agents, text, main_result, chat_id)
|
| 23 |
+
return self._synthesize_results(text, main_result, agent_results, chat_id)
|
| 24 |
+
|
| 25 |
+
def _auto_select_agents(self, text: str) -> List[str]:
|
| 26 |
+
text_lower = text.lower()
|
| 27 |
+
selected = ["universal"]
|
| 28 |
+
if any(k in text_lower for k in ["код", "code", "python", "файл", "script"]):
|
| 29 |
+
selected.extend(["guru", "hacker"])
|
| 30 |
+
if any(k in text_lower for k in ["тест", "test", "bug", "bug"]):
|
| 31 |
+
selected.extend(["qa", "sdet"])
|
| 32 |
+
if any(k in text_lower for k in ["архитектур", "architect", "design", "систем"]):
|
| 33 |
+
selected.append("architect")
|
| 34 |
+
if any(k in text_lower for k in ["review", "ревью", "audit", "аудит"]):
|
| 35 |
+
selected.extend(["techlead", "critic"])
|
| 36 |
+
return list(dict.fromkeys(selected))[:4]
|
| 37 |
+
|
| 38 |
+
def _execute_main(self, text: str, file_context: str, chat_id: str) -> str:
|
| 39 |
+
role = self.state.roles.get(self.state.current_role, self.state.roles["universal"])
|
| 40 |
+
model_name = self.state.get_model_for_role(self.state.current_role)
|
| 41 |
+
model = self.state.models.get(model_name, self.state.models["deepseek-v4-pro"])
|
| 42 |
+
agent = UniversalAgent(role, model, use_interpreter=True)
|
| 43 |
+
full_task = f"{text}\n\n{file_context}".strip()
|
| 44 |
+
return agent.execute(full_task, chat_id=chat_id, mode="skill")
|
| 45 |
+
|
| 46 |
+
def _run_agents_parallel(self, agents: List[str], text: str, main_result: str, chat_id: str) -> Dict[str, str]:
|
| 47 |
+
results = {}
|
| 48 |
+
threads = []
|
| 49 |
+
|
| 50 |
+
def run_agent(agent_name):
|
| 51 |
+
if PROCESS_MANAGER.is_cancelled():
|
| 52 |
+
results[agent_name] = "Cancelled"
|
| 53 |
+
return
|
| 54 |
+
role = self.state.roles.get(agent_name, self.state.roles["universal"])
|
| 55 |
+
model_name = self.state.get_model_for_role(agent_name)
|
| 56 |
+
model = self.state.models.get(model_name, self.state.models["deepseek-v4-pro"])
|
| 57 |
+
agent = UniversalAgent(role, model)
|
| 58 |
+
prompt = f"Original task: {text}\n\nMain agent result: {main_result[:2000]}\n\nProvide your specialized perspective as {agent_name}."
|
| 59 |
+
try:
|
| 60 |
+
results[agent_name] = agent.execute(prompt, chat_id=chat_id, mode="skill")
|
| 61 |
+
except Exception as e:
|
| 62 |
+
results[agent_name] = f"Error: {e}"
|
| 63 |
+
|
| 64 |
+
for name in agents[1:]:
|
| 65 |
+
t = threading.Thread(target=run_agent, args=(name,))
|
| 66 |
+
threads.append(t)
|
| 67 |
+
PROCESS_MANAGER.register_thread(t)
|
| 68 |
+
t.start()
|
| 69 |
+
|
| 70 |
+
for t in threads:
|
| 71 |
+
t.join(timeout=90)
|
| 72 |
+
|
| 73 |
+
return results
|
| 74 |
+
|
| 75 |
+
def _synthesize_results(self, text: str, main_result: str, agent_results: Dict[str, str], chat_id: str) -> str:
|
| 76 |
+
synthesis = f"Synthesize results for: {text}\n\nMain result:\n{main_result[:1500]}\n\nAdditional perspectives:\n"
|
| 77 |
+
for name, result in agent_results.items():
|
| 78 |
+
synthesis += f"\n--- {name} ---\n{result[:1000]}\n"
|
| 79 |
+
|
| 80 |
+
synth_role = self.state.roles.get("universal", list(self.state.roles.values())[0])
|
| 81 |
+
model_name = self.state.get_best_model(rank_by="reasoning", max_tier=2)
|
| 82 |
+
model = self.state.models.get(model_name, self.state.models["deepseek-v4-pro"])
|
| 83 |
+
agent = UniversalAgent(synth_role, model)
|
| 84 |
+
try:
|
| 85 |
+
return agent.execute(synthesis, chat_id=chat_id, mode="skill")
|
| 86 |
+
except Exception as e:
|
| 87 |
+
return main_result + "\n\n--- Additional ---\n" + "\n".join(f"{k}: {v[:500]}" for k, v in agent_results.items())
|
state.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Глобальное состояние PinkSky"""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from typing import Dict, List, Any, Optional
|
| 7 |
+
from .models import ModelConfig, Role, Conductor
|
| 8 |
+
from .model_ranking import MODEL_RANKING
|
| 9 |
+
from .config import ROLES_FILE, MODELS_FILE, CONDUCTORS_FILE, HISTORY_FILE
|
| 10 |
+
|
| 11 |
+
class PinkSkyState:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.models: Dict[str, ModelConfig] = {}
|
| 14 |
+
self.roles: Dict[str, Role] = {}
|
| 15 |
+
self.conductors: Dict[str, Conductor] = {}
|
| 16 |
+
self.current_mode: str = "chat"
|
| 17 |
+
self.current_conductor: str = "default"
|
| 18 |
+
self.current_role: str = "universal"
|
| 19 |
+
self.current_model: str = "deepseek-v4-pro"
|
| 20 |
+
self.chat_history: List[Dict[str, str]] = []
|
| 21 |
+
self.skill_history: List[Dict[str, str]] = []
|
| 22 |
+
self.build_history: List[Dict[str, str]] = []
|
| 23 |
+
self.build_context: Dict[str, Any] = {
|
| 24 |
+
"spec": "", "agents": 3, "models_tier": "tier1",
|
| 25 |
+
"skills_count": 2, "files_count": 3, "role": "universal",
|
| 26 |
+
"strategy": "parallel", "use_interpreter": True,
|
| 27 |
+
"notifications": True, "internet_access": True
|
| 28 |
+
}
|
| 29 |
+
self.cancel_flag: bool = False
|
| 30 |
+
self.load_all()
|
| 31 |
+
|
| 32 |
+
def load_all(self):
|
| 33 |
+
self._load_models()
|
| 34 |
+
self._load_roles()
|
| 35 |
+
self._load_conductors()
|
| 36 |
+
self._load_history()
|
| 37 |
+
|
| 38 |
+
def _build_model_config(self, name: str, data: dict) -> ModelConfig:
|
| 39 |
+
return ModelConfig(
|
| 40 |
+
name=name, provider="openai", endpoint=data["endpoint"],
|
| 41 |
+
api_key_env="NVIDIA_API_KEY",
|
| 42 |
+
context_window=data.get("context_window", 32000),
|
| 43 |
+
max_tokens=data.get("max_tokens", 8000),
|
| 44 |
+
cost_per_1k_input=data.get("cost_per_1k_input", 0.0),
|
| 45 |
+
cost_per_1k_output=data.get("cost_per_1k_output", 0.0),
|
| 46 |
+
coding_rank=data.get("coding_rank", 50),
|
| 47 |
+
speed_rank=data.get("speed_rank", 50),
|
| 48 |
+
reasoning_rank=data.get("reasoning_rank", 50),
|
| 49 |
+
tags=data.get("tags", [])
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
def _load_models(self):
|
| 53 |
+
defaults = {name: self._build_model_config(name, data) for name, data in MODEL_RANKING.items()}
|
| 54 |
+
if os.path.exists(MODELS_FILE):
|
| 55 |
+
try:
|
| 56 |
+
with open(MODELS_FILE, "r", encoding="utf-8") as f:
|
| 57 |
+
custom = json.load(f)
|
| 58 |
+
for k, v in custom.items():
|
| 59 |
+
if k not in defaults:
|
| 60 |
+
defaults[k] = ModelConfig(**v)
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"⚠️ Ошибка загрузки models.json: {e}")
|
| 63 |
+
self.models = defaults
|
| 64 |
+
|
| 65 |
+
def _load_roles(self):
|
| 66 |
+
defaults = {
|
| 67 |
+
"universal": Role(
|
| 68 |
+
name="universal",
|
| 69 |
+
prompt="You are PinkSky -- a universal AI assistant and autonomous developer. You help users with any tasks, scripts, theory, and project creation from scratch.",
|
| 70 |
+
description="Universal assistant for any tasks",
|
| 71 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "qwen3.5-397b"],
|
| 72 |
+
complexity="medium",
|
| 73 |
+
tags=["general"]
|
| 74 |
+
),
|
| 75 |
+
"guru": Role(
|
| 76 |
+
name="guru",
|
| 77 |
+
prompt="You are Guru Programmer PinkSky. 15+ years experience. Write elegant, production-ready code. Principles: KISS, explicit > implicit, composition > inheritance, PEP8, type hints, docstrings. Format: analysis -> code -> explanations -> edge cases.",
|
| 78 |
+
description="Guru programmer. Elegant code with deep explanations.",
|
| 79 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
|
| 80 |
+
complexity="high",
|
| 81 |
+
tags=["coding", "senior", "mentor", "python"]
|
| 82 |
+
),
|
| 83 |
+
"hacker": Role(
|
| 84 |
+
name="hacker",
|
| 85 |
+
prompt="You are Hacker PinkSky. Code virtuoso. Find elegant and unconventional solutions. Use __slots__, descriptors, metaclasses. Optimize time complexity, memory layout. Love functional: itertools, functools, operator.",
|
| 86 |
+
description="Hacker-coder. Optimization and unconventional solutions.",
|
| 87 |
+
preferred_models=["deepseek-v4-pro", "deepseek-v4-flash", "llama-4-maverick", "nemotron-super-49b"],
|
| 88 |
+
complexity="high",
|
| 89 |
+
tags=["coding", "optimization", "hacks", "performance"]
|
| 90 |
+
),
|
| 91 |
+
"architect": Role(
|
| 92 |
+
name="architect",
|
| 93 |
+
prompt="You are Software Architect PinkSky. Design systems that last years. Bounded contexts, aggregates, CQRS, Event Sourcing. API: REST, gRPC, GraphQL, WebSocket. Observability: logs, metrics, tracing from the start.",
|
| 94 |
+
description="Software Architect. High-level system design.",
|
| 95 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "nemotron-3-super", "qwen3.5-397b"],
|
| 96 |
+
complexity="high",
|
| 97 |
+
tags=["architecture", "design", "system", "ddd"]
|
| 98 |
+
),
|
| 99 |
+
"principal": Role(
|
| 100 |
+
name="principal",
|
| 101 |
+
prompt="You are Principal Engineer PinkSky. Solve problems no one else can. Refactor legacy without downtime. Platform-level: CI/CD, observability, service mesh. Engineering culture: code review, RFC process. ADR for all decisions.",
|
| 102 |
+
description="Principal engineer. Strategy, mentorship, hard problems.",
|
| 103 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
|
| 104 |
+
complexity="high",
|
| 105 |
+
tags=["leadership", "strategy", "mentoring", "legacy"]
|
| 106 |
+
),
|
| 107 |
+
"evangelist": Role(
|
| 108 |
+
name="evangelist",
|
| 109 |
+
prompt="You are Quality Evangelist PinkSky. TDD, BDD, property-based testing, mutation testing. pytest, hypothesis, coverage, mypy, ruff, bandit. Test pyramid: unit -> integration -> e2e. CI/CD gates: coverage threshold, mutation score.",
|
| 110 |
+
description="Quality evangelist. Testing and quality culture.",
|
| 111 |
+
preferred_models=["kimi-k2.6", "deepseek-v4-pro", "mistral-medium-3.5"],
|
| 112 |
+
complexity="high",
|
| 113 |
+
tags=["quality", "testing", "tdd", "ci-cd"]
|
| 114 |
+
),
|
| 115 |
+
"techlead": Role(
|
| 116 |
+
name="techlead",
|
| 117 |
+
prompt="You are Tech Lead PinkSky. Code review: correctness, readability, maintainability, security, performance. Find race conditions, memory leaks, injection points, N+1. must-fix vs should-fix vs nitpick. Code review = teaching, not tribunal.",
|
| 118 |
+
description="Tech Lead. Code review and team direction.",
|
| 119 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
|
| 120 |
+
complexity="high",
|
| 121 |
+
tags=["review", "leadership", "team", "mentoring"]
|
| 122 |
+
),
|
| 123 |
+
"qa": Role(
|
| 124 |
+
name="qa",
|
| 125 |
+
prompt="You are QA Engineer PinkSky. Test cases: positive, negative, boundary, exploratory. Equivalence partitioning, boundary value analysis. Automation: Selenium, Playwright, Postman. Performance: k6, Locust. Security: OWASP Top 10.",
|
| 126 |
+
description="QA engineer. Bug hunting and test strategy.",
|
| 127 |
+
preferred_models=["mistral-small-4", "step-3.7-flash", "llama-3.3-70b", "deepseek-v4-flash"],
|
| 128 |
+
complexity="medium",
|
| 129 |
+
tags=["qa", "testing", "automation", "manual"]
|
| 130 |
+
),
|
| 131 |
+
"sdet": Role(
|
| 132 |
+
name="sdet",
|
| 133 |
+
prompt="You are SDET PinkSky. Test frameworks: pytest plugins, custom matchers. CI/CD: parallel execution, test sharding. Test data: factories, fixtures, seeding, cleanup. Mocks/stubs/fakes: wiremock, mockserver. Test code = production code.",
|
| 134 |
+
description="SDET. Autotests and test infrastructure at dev level.",
|
| 135 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "llama-4-maverick", "mistral-medium-3.5"],
|
| 136 |
+
complexity="high",
|
| 137 |
+
tags=["sdet", "automation", "framework", "infrastructure"]
|
| 138 |
+
),
|
| 139 |
+
"qe": Role(
|
| 140 |
+
name="qe",
|
| 141 |
+
prompt="You are Quality Engineer (QE) PinkSky. Analyze SDLC: where quality is lost. Shift-left testing: quality gates at every stage. Metrics: DORA, SPACE, custom KPIs. Root cause analysis: 5 Whys, Fishbone, FMEA. Every production bug = learning opportunity.",
|
| 142 |
+
description="Quality engineer. Processes, metrics, and quality culture.",
|
| 143 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "nemotron-3-super"],
|
| 144 |
+
complexity="high",
|
| 145 |
+
tags=["qe", "process", "metrics", "culture", "sdlc"]
|
| 146 |
+
),
|
| 147 |
+
"researcher": Role(
|
| 148 |
+
name="researcher",
|
| 149 |
+
prompt="You are Researcher PinkSky. Deep topic analysis. Compare approaches: trade-offs, limitations. Structure: executive summary -> details -> sources. Identify trends. Evidence > opinions. Numbers > words.",
|
| 150 |
+
description="Researcher and analyst. Deep topic analysis.",
|
| 151 |
+
preferred_models=["deepseek-v4-pro", "qwen3.5-397b", "kimi-k2.6", "gpt-oss-120b"],
|
| 152 |
+
complexity="high",
|
| 153 |
+
tags=["research", "analysis", "comparison"]
|
| 154 |
+
),
|
| 155 |
+
"critic": Role(
|
| 156 |
+
name="critic",
|
| 157 |
+
prompt="You are Critic and Auditor PinkSky. correctness, security, performance, maintainability. race conditions, injection points, memory leaks, N+1. code smells, technical debt, architecture risks. Every issue with severity. Suggest fixes.",
|
| 158 |
+
description="Critic and auditor. Bug and issue hunting.",
|
| 159 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
|
| 160 |
+
complexity="medium",
|
| 161 |
+
tags=["audit", "security", "review", "critic"]
|
| 162 |
+
),
|
| 163 |
+
}
|
| 164 |
+
if os.path.exists(ROLES_FILE):
|
| 165 |
+
try:
|
| 166 |
+
with open(ROLES_FILE, "r", encoding="utf-8") as f:
|
| 167 |
+
custom = json.load(f)
|
| 168 |
+
for k, v in custom.items():
|
| 169 |
+
if k not in defaults:
|
| 170 |
+
defaults[k] = Role(**v)
|
| 171 |
+
except Exception as e:
|
| 172 |
+
print(f"⚠️ Ошибка загрузки roles.json: {e}")
|
| 173 |
+
self.roles = defaults
|
| 174 |
+
|
| 175 |
+
def _load_conductors(self):
|
| 176 |
+
defaults = {
|
| 177 |
+
"default": Conductor(
|
| 178 |
+
name="default",
|
| 179 |
+
prompt="""You are Conductor PinkSky (Default). Analyze request and choose optimal roles and models.
|
| 180 |
+
|
| 181 |
+
RULES:
|
| 182 |
+
1. Simple questions -- 1 role, 1 model.
|
| 183 |
+
2. Complex tasks -- decompose, assign roles.
|
| 184 |
+
3. Consider cost: cheap for simple, powerful for complex.
|
| 185 |
+
4. If code -- add critic.
|
| 186 |
+
5. If architecture -- add architect.
|
| 187 |
+
|
| 188 |
+
AVAILABLE ROLES: guru, hacker, architect, principal, evangelist, techlead, qa, sdet, qe, researcher, critic, universal.
|
| 189 |
+
|
| 190 |
+
AVAILABLE MODELS (by coding rank, best to worst):
|
| 191 |
+
TIER 1 (Elite): deepseek-v4-pro, kimi-k2.6, qwen3.5-397b, mistral-large-3, gpt-oss-120b
|
| 192 |
+
TIER 2 (Strong): deepseek-v4-flash, llama-4-maverick, nemotron-3-super, mistral-medium-3.5, dracarys-llama-70b, llama-3.3-70b, nemotron-super-49b
|
| 193 |
+
TIER 3 (Good): step-3.7-flash, mistral-small-4, minimax-m2.7, nemotron-super-49b-v1, llama-3.2-90b-vision
|
| 194 |
+
TIER 4 (Fast): nemotron-nano-12b, nemotron-3-nano-30b, nemotron-nano-9b, nemotron-content-safety
|
| 195 |
+
TIER 5 (Specialized): nemotron-3-nano-omni, diffusiongemma
|
| 196 |
+
|
| 197 |
+
FORMAT (STRICT JSON):
|
| 198 |
+
{"strategy": "single|sequential|parallel", "tasks": [{"role": "role_name", "model": "model_name", "prompt": "subtask"}], "synthesis_prompt": "how to combine"}""",
|
| 199 |
+
description="Standard conductor -- balance of quality and speed",
|
| 200 |
+
strategy="selective",
|
| 201 |
+
max_agents=3,
|
| 202 |
+
cost_aware=True,
|
| 203 |
+
auto_rank_by="balanced"
|
| 204 |
+
),
|
| 205 |
+
"strict": Conductor(
|
| 206 |
+
name="strict",
|
| 207 |
+
prompt="""You are Strict Conductor PinkSky. Minimum agents, maximum efficiency.
|
| 208 |
+
|
| 209 |
+
RULES:
|
| 210 |
+
1. ONLY one role and one model.
|
| 211 |
+
2. Cheapest model capable of solving the task.
|
| 212 |
+
3. Only sequential.
|
| 213 |
+
|
| 214 |
+
FORMAT (STRICT JSON):
|
| 215 |
+
{"strategy": "single", "tasks": [{"role": "name", "model": "name", "prompt": "task"}], "synthesis_prompt": ""}""",
|
| 216 |
+
description="Minimum agents, minimum cost",
|
| 217 |
+
strategy="single",
|
| 218 |
+
max_agents=1,
|
| 219 |
+
cost_aware=True,
|
| 220 |
+
auto_rank_by="coding"
|
| 221 |
+
),
|
| 222 |
+
"creative": Conductor(
|
| 223 |
+
name="creative",
|
| 224 |
+
prompt="""You are Creative Conductor PinkSky. Maximum perspectives, brainstorm.
|
| 225 |
+
|
| 226 |
+
RULES:
|
| 227 |
+
1. Multiple roles from different angles.
|
| 228 |
+
2. Parallel strategy.
|
| 229 |
+
3. guru + hacker + researcher + critic.
|
| 230 |
+
4. Do not save on models -- use the best.
|
| 231 |
+
|
| 232 |
+
FORMAT (STRICT JSON):
|
| 233 |
+
{"strategy": "parallel", "tasks": [...], "synthesis_prompt": "synthesize creative ideas"}""",
|
| 234 |
+
description="Maximum roles, creative brainstorm",
|
| 235 |
+
strategy="parallel",
|
| 236 |
+
max_agents=5,
|
| 237 |
+
cost_aware=False,
|
| 238 |
+
auto_rank_by="coding"
|
| 239 |
+
),
|
| 240 |
+
"economy": Conductor(
|
| 241 |
+
name="economy",
|
| 242 |
+
prompt="""You are Economy Conductor PinkSky. Solve task for minimum cost.
|
| 243 |
+
|
| 244 |
+
RULES:
|
| 245 |
+
1. Start with TIER 4 (fast/cheap): nemotron-nano-9b, nemotron-nano-12b, nemotron-3-nano-30b.
|
| 246 |
+
2. Only if it fails -- escalate to TIER 3/2.
|
| 247 |
+
3. One role, one model.
|
| 248 |
+
|
| 249 |
+
FORMAT (STRICT JSON):
|
| 250 |
+
{"strategy": "single", "tasks": [{"role": "name", "model": "name", "prompt": "task"}], "synthesis_prompt": ""}""",
|
| 251 |
+
description="Cheap models, budget saving",
|
| 252 |
+
strategy="single",
|
| 253 |
+
max_agents=1,
|
| 254 |
+
cost_aware=True,
|
| 255 |
+
auto_rank_by="speed"
|
| 256 |
+
),
|
| 257 |
+
"review": Conductor(
|
| 258 |
+
name="review",
|
| 259 |
+
prompt="""You are Code Review Conductor PinkSky. Maximum quality code review.
|
| 260 |
+
|
| 261 |
+
RULES:
|
| 262 |
+
1. techlead (architectural review) + critic (bugs/vulnerabilities) + guru (best practices).
|
| 263 |
+
2. Parallel review.
|
| 264 |
+
3. Synthesize into structured report.
|
| 265 |
+
|
| 266 |
+
FORMAT (STRICT JSON):
|
| 267 |
+
{"strategy": "parallel", "tasks": [{"role": "techlead", "model": "deepseek-v4-pro", "prompt": "architectural review"}, {"role": "critic", "model": "kimi-k2.6", "prompt": "bug hunting"}, {"role": "guru", "model": "mistral-large-3", "prompt": "best practices"}], "synthesis_prompt": "structured report with severity"}""",
|
| 268 |
+
description="Focus on code review. Multi-angle code check.",
|
| 269 |
+
strategy="parallel",
|
| 270 |
+
max_agents=4,
|
| 271 |
+
cost_aware=True,
|
| 272 |
+
auto_rank_by="coding"
|
| 273 |
+
),
|
| 274 |
+
"build": Conductor(
|
| 275 |
+
name="build",
|
| 276 |
+
prompt="""You are Project Build Conductor PinkSky. Build full project from spec.
|
| 277 |
+
|
| 278 |
+
RULES:
|
| 279 |
+
1. Sequential: architect -> guru/hacker -> sdet -> critic.
|
| 280 |
+
2. Each stage -- separate call.
|
| 281 |
+
|
| 282 |
+
FORMAT (STRICT JSON):
|
| 283 |
+
{"strategy": "sequential", "tasks": [{"role": "architect", "model": "deepseek-v4-pro", "prompt": "architecture"}, {"role": "guru", "model": "kimi-k2.6", "prompt": "code"}, {"role": "sdet", "model": "mistral-medium-3.5", "prompt": "tests"}, {"role": "critic", "model": "gpt-oss-120b", "prompt": "audit"}], "synthesis_prompt": "assemble into single project"}""",
|
| 284 |
+
description="Project build. Architecture -> code -> tests -> audit.",
|
| 285 |
+
strategy="sequential",
|
| 286 |
+
max_agents=5,
|
| 287 |
+
cost_aware=True,
|
| 288 |
+
auto_rank_by="coding"
|
| 289 |
+
),
|
| 290 |
+
}
|
| 291 |
+
if os.path.exists(CONDUCTORS_FILE):
|
| 292 |
+
try:
|
| 293 |
+
with open(CONDUCTORS_FILE, "r", encoding="utf-8") as f:
|
| 294 |
+
custom = json.load(f)
|
| 295 |
+
for k, v in custom.items():
|
| 296 |
+
if k not in defaults:
|
| 297 |
+
defaults[k] = Conductor(**v)
|
| 298 |
+
except Exception as e:
|
| 299 |
+
print(f"⚠️ Ошибка загрузки conductors.json: {e}")
|
| 300 |
+
self.conductors = defaults
|
| 301 |
+
|
| 302 |
+
def _load_history(self):
|
| 303 |
+
if os.path.exists(HISTORY_FILE):
|
| 304 |
+
try:
|
| 305 |
+
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
|
| 306 |
+
data = json.load(f)
|
| 307 |
+
self.chat_history = data.get("chat", [])
|
| 308 |
+
self.skill_history = data.get("skill", [])
|
| 309 |
+
self.build_history = data.get("build", [])
|
| 310 |
+
except Exception as e:
|
| 311 |
+
print(f"⚠️ Ошибка загрузки истории: {e}")
|
| 312 |
+
|
| 313 |
+
def save_roles(self):
|
| 314 |
+
data = {k: {"name": v.name, "prompt": v.prompt, "description": v.description,
|
| 315 |
+
"preferred_models": v.preferred_models, "complexity": v.complexity, "tags": v.tags}
|
| 316 |
+
for k, v in self.roles.items()}
|
| 317 |
+
with open(ROLES_FILE, "w", encoding="utf-8") as f:
|
| 318 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 319 |
+
|
| 320 |
+
def save_models(self):
|
| 321 |
+
data = {k: {"name": v.name, "provider": v.provider, "endpoint": v.endpoint,
|
| 322 |
+
"api_key_env": v.api_key_env, "context_window": v.context_window,
|
| 323 |
+
"max_tokens": v.max_tokens, "cost_per_1k_input": v.cost_per_1k_input,
|
| 324 |
+
"cost_per_1k_output": v.cost_per_1k_output,
|
| 325 |
+
"coding_rank": v.coding_rank, "speed_rank": v.speed_rank, "reasoning_rank": v.reasoning_rank,
|
| 326 |
+
"tags": v.tags}
|
| 327 |
+
for k, v in self.models.items()}
|
| 328 |
+
with open(MODELS_FILE, "w", encoding="utf-8") as f:
|
| 329 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 330 |
+
|
| 331 |
+
def save_conductors(self):
|
| 332 |
+
data = {k: {"name": v.name, "prompt": v.prompt, "description": v.description,
|
| 333 |
+
"strategy": v.strategy, "max_agents": v.max_agents, "cost_aware": v.cost_aware,
|
| 334 |
+
"auto_rank_by": v.auto_rank_by}
|
| 335 |
+
for k, v in self.conductors.items()}
|
| 336 |
+
with open(CONDUCTORS_FILE, "w", encoding="utf-8") as f:
|
| 337 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 338 |
+
|
| 339 |
+
def save_history(self):
|
| 340 |
+
data = {"chat": self.chat_history, "skill": self.skill_history, "build": self.build_history}
|
| 341 |
+
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
|
| 342 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 343 |
+
|
| 344 |
+
def add_to_history(self, mode: str, role: str, content: str):
|
| 345 |
+
entry = {"role": role, "content": content, "timestamp": datetime.now().isoformat()}
|
| 346 |
+
if mode == "chat":
|
| 347 |
+
self.chat_history.append(entry)
|
| 348 |
+
elif mode == "skill":
|
| 349 |
+
self.skill_history.append(entry)
|
| 350 |
+
elif mode == "build":
|
| 351 |
+
self.build_history.append(entry)
|
| 352 |
+
self.save_history()
|
| 353 |
+
|
| 354 |
+
def get_best_model(self, rank_by: str = "coding", min_tier: int = 1, max_tier: int = 5, exclude: List[str] = None) -> str:
|
| 355 |
+
exclude = exclude or []
|
| 356 |
+
candidates = []
|
| 357 |
+
for name, model in self.models.items():
|
| 358 |
+
if name in exclude or name == "hf_fallback":
|
| 359 |
+
continue
|
| 360 |
+
tier = 5
|
| 361 |
+
if model.coding_rank <= 5: tier = 1
|
| 362 |
+
elif model.coding_rank <= 12: tier = 2
|
| 363 |
+
elif model.coding_rank <= 18: tier = 3
|
| 364 |
+
elif model.coding_rank <= 24: tier = 4
|
| 365 |
+
if min_tier <= tier <= max_tier:
|
| 366 |
+
candidates.append((name, model))
|
| 367 |
+
if not candidates:
|
| 368 |
+
return "deepseek-v4-pro"
|
| 369 |
+
if rank_by == "coding":
|
| 370 |
+
candidates.sort(key=lambda x: x[1].coding_rank)
|
| 371 |
+
elif rank_by == "speed":
|
| 372 |
+
candidates.sort(key=lambda x: x[1].speed_rank)
|
| 373 |
+
elif rank_by == "reasoning":
|
| 374 |
+
candidates.sort(key=lambda x: x[1].reasoning_rank)
|
| 375 |
+
elif rank_by == "balanced":
|
| 376 |
+
candidates.sort(key=lambda x: (x[1].coding_rank + x[1].speed_rank + x[1].reasoning_rank) / 3)
|
| 377 |
+
else:
|
| 378 |
+
candidates.sort(key=lambda x: x[1].coding_rank)
|
| 379 |
+
return candidates[0][0]
|
| 380 |
+
|
| 381 |
+
def get_model_for_role(self, role_name: str, preference: str = None, rank_by: str = None) -> str:
|
| 382 |
+
role = self.roles.get(role_name)
|
| 383 |
+
if not role:
|
| 384 |
+
return preference or self.current_model
|
| 385 |
+
conductor = self.conductors.get(self.current_conductor, self.conductors["default"])
|
| 386 |
+
rank_criteria = rank_by or conductor.auto_rank_by
|
| 387 |
+
max_tier = 5
|
| 388 |
+
if role.complexity == "high":
|
| 389 |
+
max_tier = 2
|
| 390 |
+
elif role.complexity == "medium":
|
| 391 |
+
max_tier = 3
|
| 392 |
+
if preference and preference in self.models:
|
| 393 |
+
return preference
|
| 394 |
+
available = [m for m in role.preferred_models if m in self.models and m != "hf_fallback"]
|
| 395 |
+
if available:
|
| 396 |
+
if conductor.cost_aware and rank_criteria != "coding":
|
| 397 |
+
available.sort(key=lambda m: self.models[m].cost_per_1k_output)
|
| 398 |
+
else:
|
| 399 |
+
if rank_criteria == "coding":
|
| 400 |
+
available.sort(key=lambda m: self.models[m].coding_rank)
|
| 401 |
+
elif rank_criteria == "speed":
|
| 402 |
+
available.sort(key=lambda m: self.models[m].speed_rank)
|
| 403 |
+
elif rank_criteria == "reasoning":
|
| 404 |
+
available.sort(key=lambda m: self.models[m].reasoning_rank)
|
| 405 |
+
else:
|
| 406 |
+
available.sort(key=lambda m: (self.models[m].coding_rank + self.models[m].speed_rank + self.models[m].reasoning_rank) / 3)
|
| 407 |
+
return available[0]
|
| 408 |
+
return self.get_best_model(rank_by=rank_criteria, max_tier=max_tier)
|
| 409 |
+
|
| 410 |
+
def get_models_by_tier(self, tier: int) -> List[str]:
|
| 411 |
+
result = []
|
| 412 |
+
for name, model in self.models.items():
|
| 413 |
+
if name == "hf_fallback":
|
| 414 |
+
continue
|
| 415 |
+
model_tier = 5
|
| 416 |
+
if model.coding_rank <= 5: model_tier = 1
|
| 417 |
+
elif model.coding_rank <= 12: model_tier = 2
|
| 418 |
+
elif model.coding_rank <= 18: model_tier = 3
|
| 419 |
+
elif model.coding_rank <= 24: model_tier = 4
|
| 420 |
+
if model_tier == tier:
|
| 421 |
+
result.append(name)
|
| 422 |
+
return result
|
| 423 |
+
|
| 424 |
+
def get_next_tier_model(self, current_model_name: str) -> Optional[str]:
|
| 425 |
+
if current_model_name not in self.models:
|
| 426 |
+
return None
|
| 427 |
+
current = self.models[current_model_name]
|
| 428 |
+
current_tier = 5
|
| 429 |
+
if current.coding_rank <= 5: current_tier = 1
|
| 430 |
+
elif current.coding_rank <= 12: current_tier = 2
|
| 431 |
+
elif current.coding_rank <= 18: current_tier = 3
|
| 432 |
+
elif current.coding_rank <= 24: current_tier = 4
|
| 433 |
+
next_tier = current_tier + 1
|
| 434 |
+
if next_tier > 5:
|
| 435 |
+
return None
|
| 436 |
+
models_in_tier = self.get_models_by_tier(next_tier)
|
| 437 |
+
if models_in_tier:
|
| 438 |
+
return models_in_tier[0]
|
| 439 |
+
return None
|
| 440 |
+
|
| 441 |
+
def export_history_json(self) -> str:
|
| 442 |
+
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)
|
| 443 |
+
|
| 444 |
+
def export_history_md(self) -> str:
|
| 445 |
+
lines = ["# PinkSky History Export", f"
|
| 446 |
+
*Exported: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
|
| 447 |
+
"]
|
| 448 |
+
for mode, history in [("Chat", self.chat_history), ("Skill", self.skill_history), ("Build", self.build_history)]:
|
| 449 |
+
lines.append(f"
|
| 450 |
+
## {mode} Mode
|
| 451 |
+
")
|
| 452 |
+
for entry in history:
|
| 453 |
+
ts = entry.get("timestamp", "unknown")
|
| 454 |
+
role = entry.get("role", "unknown")
|
| 455 |
+
content = entry.get("content", "")
|
| 456 |
+
lines.append(f"
|
| 457 |
+
### {role} ({ts})
|
| 458 |
+
")
|
| 459 |
+
lines.append(f"```
|
| 460 |
+
{content[:500]}
|
| 461 |
+
```
|
| 462 |
+
")
|
| 463 |
+
return "
|
| 464 |
+
".join(lines)
|
| 465 |
+
|
| 466 |
+
STATE = PinkSkyState()
|
telegram_handlers.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Обработчики команд Telegram"""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import threading
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from .config import ALLOWED_USER
|
| 7 |
+
from .state import STATE
|
| 8 |
+
from .process_manager import PROCESS_MANAGER
|
| 9 |
+
from .internet_agent import INTERNET_AGENT
|
| 10 |
+
from .file_manager import FILE_MANAGER
|
| 11 |
+
from .notification_system import NOTIFICATIONS
|
| 12 |
+
from .conductor_engine import ConductorEngine
|
| 13 |
+
from .skill_orchestrator import SkillOrchestrator
|
| 14 |
+
from .build_mode_editor import BuildModeEditor
|
| 15 |
+
from .mode_handlers import run_chat_mode, run_skill_mode, run_build_mode
|
| 16 |
+
from .telegram_utils import send_tg, download_tg_file, send_tg_file
|
| 17 |
+
from .helpers import parse_skill_agents, parse_build_args, get_current_mode_info
|
| 18 |
+
|
| 19 |
+
class BotHandler:
|
| 20 |
+
HELP_TEXT = """PinkSky v7.0 — МОДУЛЬНАЯ АРХИТЕКТУРА
|
| 21 |
+
|
| 22 |
+
🎛️ Режимы:
|
| 23 |
+
Chat — отправь сообщение
|
| 24 |
+
/skill [задача] — Open Interpreter
|
| 25 |
+
/build [спек] — сборка через кондуктор
|
| 26 |
+
|
| 27 |
+
🎭 Роли:
|
| 28 |
+
/role [guru|hacker|architect|principal|evangelist|techlead|qa|sdet|qe|researcher|critic|universal]
|
| 29 |
+
/role list — список ролей
|
| 30 |
+
|
| 31 |
+
🧠 Кондукторы:
|
| 32 |
+
/conductor [default|strict|creative|economy|review|build]
|
| 33 |
+
/conductor list — список
|
| 34 |
+
|
| 35 |
+
🤖 Модели:
|
| 36 |
+
/model [имя] — переключить модель
|
| 37 |
+
/model list — список моделей
|
| 38 |
+
|
| 39 |
+
📁 Файлы:
|
| 40 |
+
/get [путь] — скачать файл
|
| 41 |
+
/stop — остановить все процессы
|
| 42 |
+
|
| 43 |
+
🌐 Интернет:
|
| 44 |
+
/search [запрос] — поиск в интернете
|
| 45 |
+
/cache — статистика кэша
|
| 46 |
+
/cache clear — очистить кэш
|
| 47 |
+
|
| 48 |
+
📊 Система:
|
| 49 |
+
/status — текущий статус
|
| 50 |
+
/history — история
|
| 51 |
+
/export [json|md] — экспорт истории
|
| 52 |
+
/mode — текущий режим"""
|
| 53 |
+
|
| 54 |
+
def __init__(self):
|
| 55 |
+
self.mode_editor = BuildModeEditor(STATE)
|
| 56 |
+
self.skill_orchestrator = SkillOrchestrator(STATE)
|
| 57 |
+
|
| 58 |
+
def handle_message(self, data: dict):
|
| 59 |
+
if "message" not in data:
|
| 60 |
+
return
|
| 61 |
+
|
| 62 |
+
message = data["message"]
|
| 63 |
+
chat_id = str(message.get("chat", {}).get("id", ""))
|
| 64 |
+
if ALLOWED_USER and chat_id != ALLOWED_USER:
|
| 65 |
+
return
|
| 66 |
+
|
| 67 |
+
NOTIFICATIONS.set_chat_id(chat_id)
|
| 68 |
+
|
| 69 |
+
text = message.get("text", message.get("caption", "")).strip()
|
| 70 |
+
|
| 71 |
+
file_id, file_name = None, None
|
| 72 |
+
target_msg = message
|
| 73 |
+
if "reply_to_message" in message:
|
| 74 |
+
if "document" in message["reply_to_message"] or "photo" in message["reply_to_message"]:
|
| 75 |
+
target_msg = message["reply_to_message"]
|
| 76 |
+
if "document" in target_msg:
|
| 77 |
+
file_id = target_msg["document"]["file_id"]
|
| 78 |
+
file_name = target_msg["document"].get("file_name", "document.file")
|
| 79 |
+
elif "photo" in target_msg:
|
| 80 |
+
file_id = target_msg["photo"][-1]["file_id"]
|
| 81 |
+
file_name = "photo.jpg"
|
| 82 |
+
|
| 83 |
+
file_context = ""
|
| 84 |
+
if file_id:
|
| 85 |
+
dl_path = download_tg_file(file_id, file_name)
|
| 86 |
+
if dl_path:
|
| 87 |
+
file_context = f"\n\n[SYSTEM: User attached file: ./{dl_path}]"
|
| 88 |
+
if not text:
|
| 89 |
+
send_tg(chat_id, f"📁 Файл `{file_name}` сохранён. Ответь командой, например: `/build Что в этом файле?`")
|
| 90 |
+
return
|
| 91 |
+
|
| 92 |
+
if not text:
|
| 93 |
+
return
|
| 94 |
+
|
| 95 |
+
parts = text.split(maxsplit=1)
|
| 96 |
+
command = parts[0].lower()
|
| 97 |
+
args = parts[1].strip() if len(parts) > 1 else ""
|
| 98 |
+
|
| 99 |
+
# === HELP ===
|
| 100 |
+
if command in ["/help", "/start"]:
|
| 101 |
+
send_tg(chat_id, self.HELP_TEXT)
|
| 102 |
+
return
|
| 103 |
+
|
| 104 |
+
# === STOP ===
|
| 105 |
+
if command == "/stop":
|
| 106 |
+
result = PROCESS_MANAGER.cancel_all()
|
| 107 |
+
send_tg(chat_id, f"{result}\n\n{self.HELP_TEXT}")
|
| 108 |
+
return
|
| 109 |
+
|
| 110 |
+
# === STATUS ===
|
| 111 |
+
if command == "/status":
|
| 112 |
+
info = get_current_mode_info()
|
| 113 |
+
stats = f"""{info}
|
| 114 |
+
|
| 115 |
+
📊 Моделей: {len(STATE.models)}
|
| 116 |
+
📊 Ролей: {len(STATE.roles)}
|
| 117 |
+
📊 Кондукторов: {len(STATE.conductors)}
|
| 118 |
+
🌐 Интернет: {'✅' if STATE.build_context.get('internet_access', True) else '❌'}
|
| 119 |
+
🗑️ Кэш: {INTERNET_AGENT.get_cache_stats()}
|
| 120 |
+
🧵 Активных потоков: {PROCESS_MANAGER.get_active_count()}"""
|
| 121 |
+
send_tg(chat_id, stats)
|
| 122 |
+
return
|
| 123 |
+
|
| 124 |
+
# === MODE ===
|
| 125 |
+
if command == "/mode":
|
| 126 |
+
send_tg(chat_id, get_current_mode_info())
|
| 127 |
+
return
|
| 128 |
+
|
| 129 |
+
# === CONDUCTOR ===
|
| 130 |
+
if command == "/conductor":
|
| 131 |
+
if args == "list":
|
| 132 |
+
lines = ["🧠 Доступные кондукторы:"]
|
| 133 |
+
for name, cond in STATE.conductors.items():
|
| 134 |
+
marker = " [ACTIVE]" if name == STATE.current_conductor else ""
|
| 135 |
+
lines.append(f"- {name}{marker}: {cond.description} [rank_by={cond.auto_rank_by}]")
|
| 136 |
+
send_tg(chat_id, "\n".join(lines))
|
| 137 |
+
return
|
| 138 |
+
if args in STATE.conductors:
|
| 139 |
+
STATE.current_conductor = args
|
| 140 |
+
send_tg(chat_id, f"🧠 Кондуктор: {args.upper()}\n{STATE.conductors[args].description}")
|
| 141 |
+
else:
|
| 142 |
+
available = ", ".join(STATE.conductors.keys())
|
| 143 |
+
send_tg(chat_id, f"Текущий: {STATE.current_conductor}\nДоступные: {available}")
|
| 144 |
+
return
|
| 145 |
+
|
| 146 |
+
# === ROLE ===
|
| 147 |
+
if command == "/role":
|
| 148 |
+
role_parts = args.split(maxsplit=1)
|
| 149 |
+
subcmd = role_parts[0].lower() if role_parts else ""
|
| 150 |
+
subargs = role_parts[1] if len(role_parts) > 1 else ""
|
| 151 |
+
|
| 152 |
+
if subcmd == "list":
|
| 153 |
+
lines = ["🎭 Доступные роли:"]
|
| 154 |
+
for name, role in STATE.roles.items():
|
| 155 |
+
marker = " [ACTIVE]" if name == STATE.current_role else ""
|
| 156 |
+
lines.append(f"- {name}{marker}: {role.description} (complexity: {role.complexity})")
|
| 157 |
+
send_tg(chat_id, "\n".join(lines))
|
| 158 |
+
return
|
| 159 |
+
|
| 160 |
+
if subcmd == "info" and subargs:
|
| 161 |
+
role = STATE.roles.get(subargs)
|
| 162 |
+
if role:
|
| 163 |
+
send_tg(chat_id, f"🎭 Роль {subargs}:\n\n{role.description}\n\nComplexity: {role.complexity}\nPreferred: {', '.join(role.preferred_models)}\n\nPrompt:\n```\n{role.prompt[:500]}...\n```")
|
| 164 |
+
else:
|
| 165 |
+
send_tg(chat_id, f"❌ Роль {subargs} не найдена")
|
| 166 |
+
return
|
| 167 |
+
|
| 168 |
+
if subcmd == "add" and subargs:
|
| 169 |
+
add_parts = subargs.split(maxsplit=1)
|
| 170 |
+
if len(add_parts) < 2:
|
| 171 |
+
send_tg(chat_id, "Формат: /role add <name> <prompt>")
|
| 172 |
+
return
|
| 173 |
+
new_name, new_prompt = add_parts[0], add_parts[1]
|
| 174 |
+
from .models import Role
|
| 175 |
+
STATE.roles[new_name] = Role(
|
| 176 |
+
name=new_name, prompt=new_prompt,
|
| 177 |
+
description=f"Custom role (added {datetime.now().strftime('%Y-%m-%d')})",
|
| 178 |
+
complexity="medium"
|
| 179 |
+
)
|
| 180 |
+
STATE.save_roles()
|
| 181 |
+
send_tg(chat_id, f"✅ Роль {new_name} добавлена!")
|
| 182 |
+
return
|
| 183 |
+
|
| 184 |
+
if subcmd in STATE.roles:
|
| 185 |
+
STATE.current_role = subcmd
|
| 186 |
+
send_tg(chat_id, f"🎭 Роль: {subcmd.upper()}\n{STATE.roles[subcmd].description}")
|
| 187 |
+
else:
|
| 188 |
+
available = ", ".join(STATE.roles.keys())
|
| 189 |
+
send_tg(chat_id, f"Текущая: {STATE.current_role}\nДоступные: {available}")
|
| 190 |
+
return
|
| 191 |
+
|
| 192 |
+
# === MODEL ===
|
| 193 |
+
if command == "/model":
|
| 194 |
+
model_parts = args.split(maxsplit=1)
|
| 195 |
+
subcmd = model_parts[0].lower() if model_parts else ""
|
| 196 |
+
subargs = model_parts[1] if len(model_parts) > 1 else ""
|
| 197 |
+
|
| 198 |
+
if subcmd == "list":
|
| 199 |
+
lines = ["🤖 Модели (по coding rank):"]
|
| 200 |
+
for name, model in sorted(STATE.models.items(), key=lambda x: x[1].coding_rank):
|
| 201 |
+
if name == "hf_fallback":
|
| 202 |
+
continue
|
| 203 |
+
marker = " [ACTIVE]" if name == STATE.current_model else ""
|
| 204 |
+
tier = 1
|
| 205 |
+
if model.coding_rank > 5: tier = 2
|
| 206 |
+
if model.coding_rank > 12: tier = 3
|
| 207 |
+
if model.coding_rank > 18: tier = 4
|
| 208 |
+
if model.coding_rank > 24: tier = 5
|
| 209 |
+
cost = f"${model.cost_per_1k_output}/1k" if model.cost_per_1k_output > 0 else "free"
|
| 210 |
+
lines.append(f"- {name}{marker} -- TIER {tier} | coding={model.coding_rank} speed={model.speed_rank} reasoning={model.reasoning_rank} | {cost}")
|
| 211 |
+
send_tg(chat_id, "\n".join(lines))
|
| 212 |
+
return
|
| 213 |
+
|
| 214 |
+
if subcmd == "add" and subargs:
|
| 215 |
+
add_parts = subargs.split()
|
| 216 |
+
if len(add_parts) < 2:
|
| 217 |
+
send_tg(chat_id, "Формат: /model add <name> <endpoint>")
|
| 218 |
+
return
|
| 219 |
+
new_name, new_endpoint = add_parts[0], add_parts[1]
|
| 220 |
+
from .models import ModelConfig
|
| 221 |
+
STATE.models[new_name] = ModelConfig(
|
| 222 |
+
name=new_name, provider="openai", endpoint=new_endpoint,
|
| 223 |
+
api_key_env="NVIDIA_API_KEY"
|
| 224 |
+
)
|
| 225 |
+
STATE.save_models()
|
| 226 |
+
send_tg(chat_id, f"✅ Модель {new_name} добавлена!")
|
| 227 |
+
return
|
| 228 |
+
|
| 229 |
+
if subcmd in STATE.models:
|
| 230 |
+
STATE.current_model = subcmd
|
| 231 |
+
send_tg(chat_id, f"🤖 Модель: {subcmd.upper()}\n{STATE.models[subcmd].endpoint}")
|
| 232 |
+
else:
|
| 233 |
+
available = ", ".join([k for k in STATE.models.keys() if k != "hf_fallback"])
|
| 234 |
+
send_tg(chat_id, f"Текущая: {STATE.current_model}\nДоступные: {available}")
|
| 235 |
+
return
|
| 236 |
+
|
| 237 |
+
# === GET FILE ===
|
| 238 |
+
if command == "/get":
|
| 239 |
+
if not args:
|
| 240 |
+
send_tg(chat_id, "Использование: /get ./projects/test.py")
|
| 241 |
+
else:
|
| 242 |
+
send_tg_file(chat_id, args)
|
| 243 |
+
return
|
| 244 |
+
|
| 245 |
+
# === SEARCH ===
|
| 246 |
+
if command == "/search":
|
| 247 |
+
if not args:
|
| 248 |
+
send_tg(chat_id, "Использование: /search запрос")
|
| 249 |
+
return
|
| 250 |
+
results = INTERNET_AGENT.search_web(args)
|
| 251 |
+
if results:
|
| 252 |
+
lines = [f"🔍 Результаты поиска: {args}\n"]
|
| 253 |
+
for i, r in enumerate(results[:5], 1):
|
| 254 |
+
lines.append(f"{i}. [{r['title']}]({r['url']})\n{r['snippet'][:150]}...\n")
|
| 255 |
+
send_tg(chat_id, "\n".join(lines))
|
| 256 |
+
else:
|
| 257 |
+
send_tg(chat_id, "❌ Ничего не найдено")
|
| 258 |
+
return
|
| 259 |
+
|
| 260 |
+
# === CACHE ===
|
| 261 |
+
if command == "/cache":
|
| 262 |
+
if args == "clear":
|
| 263 |
+
result = INTERNET_AGENT.clear_cache()
|
| 264 |
+
send_tg(chat_id, result)
|
| 265 |
+
else:
|
| 266 |
+
send_tg(chat_id, INTERNET_AGENT.get_cache_stats())
|
| 267 |
+
return
|
| 268 |
+
|
| 269 |
+
# === HISTORY ===
|
| 270 |
+
if command == "/history":
|
| 271 |
+
mode = args if args in ("chat", "skill", "build") else "chat"
|
| 272 |
+
history = getattr(STATE, f"{mode}_history", [])
|
| 273 |
+
if not history:
|
| 274 |
+
send_tg(chat_id, f"📋 История {mode} пуста")
|
| 275 |
+
return
|
| 276 |
+
lines = [f"📋 История {mode} (последние 5):"]
|
| 277 |
+
for entry in history[-5:]:
|
| 278 |
+
ts = entry.get("timestamp", "unknown")
|
| 279 |
+
role = entry.get("role", "unknown")
|
| 280 |
+
content = entry.get("content", "")[:200]
|
| 281 |
+
lines.append(f"\n[{ts}] {role}:\n{content}...")
|
| 282 |
+
send_tg(chat_id, "\n".join(lines))
|
| 283 |
+
return
|
| 284 |
+
|
| 285 |
+
# === EXPORT ===
|
| 286 |
+
if command == "/export":
|
| 287 |
+
fmt = args if args in ("json", "md") else "json"
|
| 288 |
+
if fmt == "json":
|
| 289 |
+
data = STATE.export_history_json()
|
| 290 |
+
FILE_MANAGER.save_file("exports/history.json", data)
|
| 291 |
+
send_tg_file(chat_id, "exports/history.json")
|
| 292 |
+
else:
|
| 293 |
+
data = STATE.export_history_md()
|
| 294 |
+
FILE_MANAGER.save_file("exports/history.md", data)
|
| 295 |
+
send_tg_file(chat_id, "exports/history.md")
|
| 296 |
+
return
|
| 297 |
+
|
| 298 |
+
# === BUILD MODE ===
|
| 299 |
+
if command == "/build":
|
| 300 |
+
if not args:
|
| 301 |
+
send_tg(chat_id, "Укажите спецификацию проекта.")
|
| 302 |
+
return
|
| 303 |
+
params, clean_args = parse_build_args(args)
|
| 304 |
+
run_build_mode(chat_id, clean_args, file_context, params)
|
| 305 |
+
return
|
| 306 |
+
|
| 307 |
+
# === SKILL MODE ===
|
| 308 |
+
if command == "/skill":
|
| 309 |
+
if not args:
|
| 310 |
+
send_tg(chat_id, "Укажите задачу.")
|
| 311 |
+
return
|
| 312 |
+
run_skill_mode(chat_id, args, file_context)
|
| 313 |
+
return
|
| 314 |
+
|
| 315 |
+
# === DEFAULT: CHAT MODE ===
|
| 316 |
+
run_chat_mode(chat_id, text, file_context)
|
telegram_utils.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Telegram утилиты: отправка сообщений и файлов"""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
import urllib.request
|
| 6 |
+
import time
|
| 7 |
+
import requests
|
| 8 |
+
from .config import CF_URL, TOKEN
|
| 9 |
+
|
| 10 |
+
def send_tg(chat_id, text):
|
| 11 |
+
if not CF_URL or not TOKEN:
|
| 12 |
+
return
|
| 13 |
+
safe_text = str(text)[-4000:]
|
| 14 |
+
url = f"{CF_URL}/bot{TOKEN}/sendMessage"
|
| 15 |
+
data = json.dumps({"chat_id": chat_id, "text": safe_text, "parse_mode": "Markdown"}).encode('utf-8')
|
| 16 |
+
headers = {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
|
| 17 |
+
req = urllib.request.Request(url, data=data, headers=headers)
|
| 18 |
+
try:
|
| 19 |
+
urllib.request.urlopen(req, timeout=15)
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print(f"TG send error: {e}")
|
| 22 |
+
|
| 23 |
+
def download_tg_file(file_id, file_name, retries=3):
|
| 24 |
+
if not TOKEN:
|
| 25 |
+
return None
|
| 26 |
+
import urllib3
|
| 27 |
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
| 28 |
+
base_api = CF_URL if CF_URL else "https://api.telegram.org"
|
| 29 |
+
get_file_url = f"{base_api}/bot{TOKEN}/getFile?file_id={file_id}"
|
| 30 |
+
for attempt in range(retries):
|
| 31 |
+
try:
|
| 32 |
+
res = requests.get(get_file_url, timeout=15, verify=False).json()
|
| 33 |
+
if not res.get("ok"):
|
| 34 |
+
return None
|
| 35 |
+
file_path = res["result"]["file_path"]
|
| 36 |
+
dl_url = f"{base_api}/file/bot{TOKEN}/{file_path}"
|
| 37 |
+
os.makedirs("downloads", exist_ok=True)
|
| 38 |
+
safe_name = os.path.basename(file_name)
|
| 39 |
+
local_path = os.path.join("downloads", safe_name)
|
| 40 |
+
response = requests.get(dl_url, stream=True, timeout=60, verify=False)
|
| 41 |
+
if response.status_code != 200:
|
| 42 |
+
direct_url = f"https://api.telegram.org/file/bot{TOKEN}/{file_path}"
|
| 43 |
+
response = requests.get(direct_url, stream=True, timeout=60, verify=False)
|
| 44 |
+
response.raise_for_status()
|
| 45 |
+
with open(local_path, "wb") as f:
|
| 46 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 47 |
+
if chunk:
|
| 48 |
+
f.write(chunk)
|
| 49 |
+
return local_path
|
| 50 |
+
except Exception as e:
|
| 51 |
+
time.sleep(2)
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
def send_tg_file(chat_id, file_path):
|
| 55 |
+
if not os.path.exists(file_path):
|
| 56 |
+
send_tg(chat_id, f"File not found: {file_path}")
|
| 57 |
+
return
|
| 58 |
+
base_api = CF_URL if CF_URL else "https://api.telegram.org"
|
| 59 |
+
url = f"{base_api}/bot{TOKEN}/sendDocument"
|
| 60 |
+
send_tg(chat_id, f"Sending file: {os.path.basename(file_path)}...")
|
| 61 |
+
try:
|
| 62 |
+
with open(file_path, 'rb') as f:
|
| 63 |
+
res = requests.post(url, data={'chat_id': chat_id}, files={'document': f}, verify=False, timeout=60)
|
| 64 |
+
if res.status_code == 200:
|
| 65 |
+
print(f"File {file_path} sent to TG!")
|
| 66 |
+
else:
|
| 67 |
+
send_tg(chat_id, "Failed to send file.")
|
| 68 |
+
except Exception as e:
|
| 69 |
+
send_tg(chat_id, f"Send error: {e}")
|
universal_agent.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Универсальный агент с поддержкой Open Interpreter"""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import urllib.request
|
| 5 |
+
import re
|
| 6 |
+
from typing import Dict, List, Optional, Any
|
| 7 |
+
from .models import Role, ModelConfig
|
| 8 |
+
from .config import API_KEY, API_BASE, HF_TOKEN
|
| 9 |
+
from .file_manager import FILE_MANAGER
|
| 10 |
+
from .internet_agent import INTERNET_AGENT
|
| 11 |
+
from .notification_system import NOTIFICATIONS
|
| 12 |
+
from .process_manager import PROCESS_MANAGER
|
| 13 |
+
from .state import STATE
|
| 14 |
+
|
| 15 |
+
class UniversalAgent:
|
| 16 |
+
def __init__(self, role: Role, model: ModelConfig, use_interpreter: bool = False):
|
| 17 |
+
self.role = role
|
| 18 |
+
self.model = model
|
| 19 |
+
self.use_interpreter = use_interpreter
|
| 20 |
+
self.conversation_history: List[Dict[str, str]] = []
|
| 21 |
+
self.file_manager = FILE_MANAGER
|
| 22 |
+
self.internet = INTERNET_AGENT
|
| 23 |
+
self.notifications = NOTIFICATIONS
|
| 24 |
+
self.tools = {
|
| 25 |
+
"search_web": self.internet.search_web,
|
| 26 |
+
"fetch_page": self.internet.fetch_page,
|
| 27 |
+
"analyze_website": self.internet.analyze_website,
|
| 28 |
+
"read_file": self.file_manager.read_file,
|
| 29 |
+
"save_file": self.file_manager.save_file,
|
| 30 |
+
"list_files": self.file_manager.list_files,
|
| 31 |
+
"analyze_file": self.file_manager.analyze_file,
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
def _add_tools_to_task(self, task: str) -> str:
|
| 35 |
+
tools_desc = "\n\nAVAILABLE TOOLS:\n"
|
| 36 |
+
for name, func in self.tools.items():
|
| 37 |
+
tools_desc += f"- {name}: {func.__doc__ or 'No description'}\n"
|
| 38 |
+
tools_desc += "\nUse tools when needed. Return results in natural language."
|
| 39 |
+
return task + tools_desc
|
| 40 |
+
|
| 41 |
+
def execute(self, task: str, sys_prompt_override: str = None, chat_id: str = None,
|
| 42 |
+
history: List[Dict[str, str]] = None, mode: str = "chat") -> str:
|
| 43 |
+
if self.use_interpreter and mode in ("skill", "build"):
|
| 44 |
+
return self._execute_with_interpreter(task, chat_id, mode)
|
| 45 |
+
return self._execute_with_api(task, sys_prompt_override, chat_id, history, mode)
|
| 46 |
+
|
| 47 |
+
def _execute_with_interpreter(self, task: str, chat_id: str = None, mode: str = "chat") -> str:
|
| 48 |
+
try:
|
| 49 |
+
from interpreter import interpreter
|
| 50 |
+
configure_interpreter_for_model(self.model.name)
|
| 51 |
+
base_instructions = interpreter.custom_instructions or ""
|
| 52 |
+
interpreter.custom_instructions = base_instructions + f"\n\nCURRENT ROLE: {self.role.name}\n{self.role.prompt}"
|
| 53 |
+
messages = interpreter.chat(task, display=False)
|
| 54 |
+
interpreter.custom_instructions = base_instructions
|
| 55 |
+
if messages and len(messages) > 0:
|
| 56 |
+
return messages[-1].get("content", "Done")
|
| 57 |
+
return "No response from interpreter"
|
| 58 |
+
except Exception as e:
|
| 59 |
+
return f"Interpreter error: {e}. Falling back to API..."
|
| 60 |
+
|
| 61 |
+
def _execute_with_api(self, task: str, sys_prompt_override: str = None, chat_id: str = None,
|
| 62 |
+
history: List[Dict[str, str]] = None, mode: str = "chat") -> str:
|
| 63 |
+
system_prompt = sys_prompt_override or self.role.prompt
|
| 64 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 65 |
+
if history:
|
| 66 |
+
for h in history[-10:]:
|
| 67 |
+
messages.append({"role": h.get("role", "user"), "content": h.get("content", "")})
|
| 68 |
+
messages.append({"role": "user", "content": task})
|
| 69 |
+
|
| 70 |
+
if self.model.provider == "hf":
|
| 71 |
+
return self._call_hf(messages, chat_id)
|
| 72 |
+
return self._call_openai_compatible(messages, chat_id)
|
| 73 |
+
|
| 74 |
+
def _call_openai_compatible(self, messages: List[Dict], chat_id: str = None) -> str:
|
| 75 |
+
url = f"{API_BASE}/chat/completions"
|
| 76 |
+
data = json.dumps({
|
| 77 |
+
"model": self.model.endpoint,
|
| 78 |
+
"messages": messages,
|
| 79 |
+
"temperature": 0.7,
|
| 80 |
+
"max_tokens": self.model.max_tokens
|
| 81 |
+
}).encode('utf-8')
|
| 82 |
+
req = urllib.request.Request(url, data=data, headers={
|
| 83 |
+
"Authorization": f"Bearer {API_KEY}",
|
| 84 |
+
"Content-Type": "application/json"
|
| 85 |
+
})
|
| 86 |
+
try:
|
| 87 |
+
with urllib.request.urlopen(req, timeout=45) as response:
|
| 88 |
+
res = json.loads(response.read().decode('utf-8'))
|
| 89 |
+
return res['choices'][0]['message']['content']
|
| 90 |
+
except Exception as primary_error:
|
| 91 |
+
return self._tiered_fallback(messages, primary_error, chat_id)
|
| 92 |
+
|
| 93 |
+
def _call_hf(self, messages: List[Dict], chat_id: str = None) -> str:
|
| 94 |
+
url = "https://api-inference.huggingface.co/v1/chat/completions"
|
| 95 |
+
data = json.dumps({
|
| 96 |
+
"model": self.model.endpoint,
|
| 97 |
+
"messages": messages,
|
| 98 |
+
"temperature": 0.7,
|
| 99 |
+
"max_tokens": self.model.max_tokens
|
| 100 |
+
}).encode('utf-8')
|
| 101 |
+
req = urllib.request.Request(url, data=data, headers={
|
| 102 |
+
"Authorization": f"Bearer {HF_TOKEN}",
|
| 103 |
+
"Content-Type": "application/json"
|
| 104 |
+
})
|
| 105 |
+
try:
|
| 106 |
+
with urllib.request.urlopen(req, timeout=60) as response:
|
| 107 |
+
res = json.loads(response.read().decode('utf-8'))
|
| 108 |
+
return res['choices'][0]['message']['content']
|
| 109 |
+
except Exception as e:
|
| 110 |
+
return f"HF API Error: {e}"
|
| 111 |
+
|
| 112 |
+
def _tiered_fallback(self, messages: List[Dict], primary_error, chat_id: str = None) -> str:
|
| 113 |
+
if chat_id:
|
| 114 |
+
from .telegram_utils import send_tg
|
| 115 |
+
send_tg(chat_id, f"⚠️ {self.model.name} failed: {primary_error}. Trying fallback...")
|
| 116 |
+
tried = [self.model.name]
|
| 117 |
+
current = self.model.name
|
| 118 |
+
while True:
|
| 119 |
+
next_model = STATE.get_next_tier_model(current)
|
| 120 |
+
if not next_model or next_model in tried:
|
| 121 |
+
break
|
| 122 |
+
tried.append(next_model)
|
| 123 |
+
model_cfg = STATE.models.get(next_model)
|
| 124 |
+
if not model_cfg:
|
| 125 |
+
break
|
| 126 |
+
try:
|
| 127 |
+
agent = UniversalAgent(self.role, model_cfg)
|
| 128 |
+
result = agent._call_hf(messages, chat_id) if model_cfg.provider == "hf" else agent._call_openai_compatible(messages, chat_id)
|
| 129 |
+
return result + f"\n\n_(Fallback via {next_model})_"
|
| 130 |
+
except Exception as e:
|
| 131 |
+
current = next_model
|
| 132 |
+
continue
|
| 133 |
+
return self._fallback_hf(messages, primary_error, chat_id, tried)
|
| 134 |
+
|
| 135 |
+
def _fallback_hf(self, messages: List[Dict], primary_error, chat_id: str = None, tried_models: List[str] = None) -> str:
|
| 136 |
+
if not HF_TOKEN:
|
| 137 |
+
return f"API Error: {primary_error}. No HF_TOKEN for backup."
|
| 138 |
+
fallback = STATE.models.get("hf_fallback")
|
| 139 |
+
if not fallback:
|
| 140 |
+
return f"API Error: {primary_error}. HF fallback not configured."
|
| 141 |
+
try:
|
| 142 |
+
result = self._call_hf(messages, chat_id)
|
| 143 |
+
return result + "\n\n_(Context saved via HF Serverless)_"
|
| 144 |
+
except Exception as hf_error:
|
| 145 |
+
return f"Both systems failed.\n1. API: {primary_error}\n2. HF Fallback: {hf_error}"
|