FreshPixels commited on
Commit
896124d
·
verified ·
1 Parent(s): 1ea667d

Upload 19 files

Browse files
__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}"