PinkSky / server /telegram_handlers.py
FreshPixels's picture
Update server/telegram_handlers.py
8973f60 verified
Raw
History Blame Contribute Delete
13.3 kB
"""Обработчики команд Telegram"""
import json
import threading
from datetime import datetime
from .config import ALLOWED_USER
from .state import STATE
from .process_manager import PROCESS_MANAGER
from . import INTERNET_AGENT
from .file_manager import FILE_MANAGER
from .notification_system import NOTIFICATIONS
from .conductor_engine import ConductorEngine
from .skill_orchestrator import SkillOrchestrator
from .build_mode_editor import BuildModeEditor
from .mode_handlers import run_chat_mode, run_skill_mode, run_build_mode
from .telegram_utils import send_tg, download_tg_file, send_tg_file
from .helpers import parse_skill_agents, parse_build_args, get_current_mode_info
class BotHandler:
HELP_TEXT = """PinkSky v7.0 — МОДУЛЬНАЯ АРХИТЕКТУРА
🎛️ Режимы:
Chat — отправь сообщение
/skill [задача] — Open Interpreter
/build [спек] — сборка через кондуктор
🎭 Роли:
/role [guru|hacker|architect|principal|evangelist|techlead|qa|sdet|qe|researcher|critic|universal]
/role list — список ролей
🧠 Кондукторы:
/conductor [default|strict|creative|economy|review|build]
/conductor list — список
🤖 Модели:
/model [имя] — переключить модель
/model list — список моделей
📁 Файлы:
/get [путь] — скачать файл
/stop — остановить все процессы
🌐 Интернет:
/search [запрос] — поиск в интернете
/cache — статистика кэша
/cache clear — очистить кэш
📊 Система:
/status — текущий статус
/history — история
/export [json|md] — экспорт истории
/mode — текущий режим"""
def __init__(self, *args, **kwargs):
# Принимаем args/kwargs для совместимости с BaseHTTPRequestHandler.__init__
# BotHandler не использует их напрямую
self.mode_editor = BuildModeEditor(STATE)
self.skill_orchestrator = SkillOrchestrator(STATE)
def handle_message(self, data: dict):
if "message" not in data:
return
message = data["message"]
chat_id = str(message.get("chat", {}).get("id", ""))
if ALLOWED_USER and chat_id != ALLOWED_USER:
return
NOTIFICATIONS.set_chat_id(chat_id)
text = message.get("text", message.get("caption", "")).strip()
file_id, file_name = None, None
target_msg = message
if "reply_to_message" in message:
if "document" in message["reply_to_message"] or "photo" in message["reply_to_message"]:
target_msg = message["reply_to_message"]
if "document" in target_msg:
file_id = target_msg["document"]["file_id"]
file_name = target_msg["document"].get("file_name", "document.file")
elif "photo" in target_msg:
file_id = target_msg["photo"][-1]["file_id"]
file_name = "photo.jpg"
file_context = ""
if file_id:
dl_path = download_tg_file(file_id, file_name)
if dl_path:
file_context = f"\n\n[SYSTEM: User attached file: ./{dl_path}]"
if not text:
send_tg(chat_id, f"📁 Файл `{file_name}` сохранён. Ответь командой, например: `/build Что в этом файле?`")
return
if not text:
return
parts = text.split(maxsplit=1)
command = parts[0].lower()
args = parts[1].strip() if len(parts) > 1 else ""
# === HELP ===
if command in ["/help", "/start"]:
send_tg(chat_id, self.HELP_TEXT)
return
# === STOP ===
if command == "/stop":
result = PROCESS_MANAGER.cancel_all()
send_tg(chat_id, f"{result}\n\n{self.HELP_TEXT}")
return
# === STATUS ===
if command == "/status":
info = get_current_mode_info()
stats = f"""{info}
📊 Моделей: {len(STATE.models)}
📊 Ролей: {len(STATE.roles)}
📊 Кондукторов: {len(STATE.conductors)}
🌐 Интернет: {'✅' if STATE.build_context.get('internet_access', True) else '❌'}
🗑️ Кэш: {INTERNET_AGENT.get_cache_stats()}
🧵 Активных потоков: {PROCESS_MANAGER.get_active_count()}"""
send_tg(chat_id, stats)
return
# === MODE ===
if command == "/mode":
send_tg(chat_id, get_current_mode_info())
return
# === CONDUCTOR ===
if command == "/conductor":
if args == "list":
lines = ["🧠 Доступные кондукторы:"]
for name, cond in STATE.conductors.items():
marker = " [ACTIVE]" if name == STATE.current_conductor else ""
lines.append(f"- {name}{marker}: {cond.description} [rank_by={cond.auto_rank_by}]")
send_tg(chat_id, "\n".join(lines))
return
if args in STATE.conductors:
STATE.current_conductor = args
send_tg(chat_id, f"🧠 Кондуктор: {args.upper()}\n{STATE.conductors[args].description}")
else:
available = ", ".join(STATE.conductors.keys())
send_tg(chat_id, f"Текущий: {STATE.current_conductor}\nДоступные: {available}")
return
# === ROLE ===
if command == "/role":
role_parts = args.split(maxsplit=1)
subcmd = role_parts[0].lower() if role_parts else ""
subargs = role_parts[1] if len(role_parts) > 1 else ""
if subcmd == "list":
lines = ["🎭 Доступные роли:"]
for name, role in STATE.roles.items():
marker = " [ACTIVE]" if name == STATE.current_role else ""
lines.append(f"- {name}{marker}: {role.description} (complexity: {role.complexity})")
send_tg(chat_id, "\n".join(lines))
return
if subcmd == "info" and subargs:
role = STATE.roles.get(subargs)
if role:
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```")
else:
send_tg(chat_id, f"❌ Роль {subargs} не найдена")
return
if subcmd == "add" and subargs:
add_parts = subargs.split(maxsplit=1)
if len(add_parts) < 2:
send_tg(chat_id, "Формат: /role add <name> <prompt>")
return
new_name, new_prompt = add_parts[0], add_parts[1]
from .models import Role
STATE.roles[new_name] = Role(
name=new_name, prompt=new_prompt,
description=f"Custom role (added {datetime.now().strftime('%Y-%m-%d')})",
complexity="medium"
)
STATE.save_roles()
send_tg(chat_id, f"✅ Роль {new_name} добавлена!")
return
if subcmd in STATE.roles:
STATE.current_role = subcmd
send_tg(chat_id, f"🎭 Роль: {subcmd.upper()}\n{STATE.roles[subcmd].description}")
else:
available = ", ".join(STATE.roles.keys())
send_tg(chat_id, f"Текущая: {STATE.current_role}\nДоступные: {available}")
return
# === MODEL ===
if command == "/model":
model_parts = args.split(maxsplit=1)
subcmd = model_parts[0].lower() if model_parts else ""
subargs = model_parts[1] if len(model_parts) > 1 else ""
if subcmd == "list":
lines = ["🤖 Модели (по coding rank):"]
for name, model in sorted(STATE.models.items(), key=lambda x: x[1].coding_rank):
if name == "hf_fallback":
continue
marker = " [ACTIVE]" if name == STATE.current_model else ""
tier = 1
if model.coding_rank > 5: tier = 2
if model.coding_rank > 12: tier = 3
if model.coding_rank > 18: tier = 4
if model.coding_rank > 24: tier = 5
cost = f"${model.cost_per_1k_output}/1k" if model.cost_per_1k_output > 0 else "free"
lines.append(f"- {name}{marker} -- TIER {tier} | coding={model.coding_rank} speed={model.speed_rank} reasoning={model.reasoning_rank} | {cost}")
send_tg(chat_id, "\n".join(lines))
return
if subcmd == "add" and subargs:
add_parts = subargs.split()
if len(add_parts) < 2:
send_tg(chat_id, "Формат: /model add <name> <endpoint>")
return
new_name, new_endpoint = add_parts[0], add_parts[1]
from .models import ModelConfig
STATE.models[new_name] = ModelConfig(
name=new_name, provider="openai", endpoint=new_endpoint,
api_key_env="NVIDIA_API_KEY"
)
STATE.save_models()
send_tg(chat_id, f"✅ Модель {new_name} добавлена!")
return
if subcmd in STATE.models:
STATE.current_model = subcmd
send_tg(chat_id, f"🤖 Модель: {subcmd.upper()}\n{STATE.models[subcmd].endpoint}")
else:
available = ", ".join([k for k in STATE.models.keys() if k != "hf_fallback"])
send_tg(chat_id, f"Текущая: {STATE.current_model}\nДоступные: {available}")
return
# === GET FILE ===
if command == "/get":
if not args:
send_tg(chat_id, "Использование: /get ./projects/test.py")
else:
send_tg_file(chat_id, args)
return
# === SEARCH ===
if command == "/search":
if not args:
send_tg(chat_id, "Использование: /search запрос")
return
results = INTERNET_AGENT.search_web(args)
if results:
lines = [f"🔍 Результаты поиска: {args}\n"]
for i, r in enumerate(results[:5], 1):
lines.append(f"{i}. [{r['title']}]({r['url']})\n{r['snippet'][:150]}...\n")
send_tg(chat_id, "\n".join(lines))
else:
send_tg(chat_id, "❌ Ничего не найдено")
return
# === CACHE ===
if command == "/cache":
if args == "clear":
result = INTERNET_AGENT.clear_cache()
send_tg(chat_id, result)
else:
send_tg(chat_id, INTERNET_AGENT.get_cache_stats())
return
# === HISTORY ===
if command == "/history":
mode = args if args in ("chat", "skill", "build") else "chat"
history = getattr(STATE, f"{mode}_history", [])
if not history:
send_tg(chat_id, f"📋 История {mode} пуста")
return
lines = [f"📋 История {mode} (последние 5):"]
for entry in history[-5:]:
ts = entry.get("timestamp", "unknown")
role = entry.get("role", "unknown")
content = entry.get("content", "")[:200]
lines.append(f"\n[{ts}] {role}:\n{content}...")
send_tg(chat_id, "\n".join(lines))
return
# === EXPORT ===
if command == "/export":
fmt = args if args in ("json", "md") else "json"
if fmt == "json":
data = STATE.export_history_json()
FILE_MANAGER.save_file("exports/history.json", data)
send_tg_file(chat_id, "exports/history.json")
else:
data = STATE.export_history_md()
FILE_MANAGER.save_file("exports/history.md", data)
send_tg_file(chat_id, "exports/history.md")
return
# === BUILD MODE ===
if command == "/build":
if not args:
send_tg(chat_id, "Укажите спецификацию проекта.")
return
params, clean_args = parse_build_args(args)
run_build_mode(chat_id, clean_args, file_context, params)
return
# === SKILL MODE ===
if command == "/skill":
if not args:
send_tg(chat_id, "Укажите задачу.")
return
run_skill_mode(chat_id, args, file_context)
return
# === DEFAULT: CHAT MODE ===
run_chat_mode(chat_id, text, file_context)