Spaces:
Runtime error
Runtime error
Upload 2 files
Browse files- notification_system.py +86 -0
- state.py +475 -0
notification_system.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Ленивый импорт — чтобы избежать циклических зависимостей при старте
|
| 73 |
+
from .telegram_utils import send_tg
|
| 74 |
+
send_tg(self.chat_id, message)
|
| 75 |
+
return f"✅ Уведомление отправлено: {action}"
|
| 76 |
+
|
| 77 |
+
def get_history(self, limit: int = 10) -> str:
|
| 78 |
+
if not self.notification_history:
|
| 79 |
+
return "📋 Нет уведомлений"
|
| 80 |
+
result = "📋 *История уведомлений:*\n\n"
|
| 81 |
+
for entry in self.notification_history[-limit:]:
|
| 82 |
+
emoji = {'critical': '🚨', 'warning': '⚠️', 'info': 'ℹ️'}.get(entry['severity'], 'ℹ️')
|
| 83 |
+
result += f"{emoji} `{entry['action']}` — {entry['timestamp']}\n"
|
| 84 |
+
return result
|
| 85 |
+
|
| 86 |
+
NOTIFICATIONS = NotificationSystem()
|
state.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
_instance = None
|
| 13 |
+
_initialized = False
|
| 14 |
+
|
| 15 |
+
def __new__(cls):
|
| 16 |
+
if cls._instance is None:
|
| 17 |
+
cls._instance = super().__new__(cls)
|
| 18 |
+
return cls._instance
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
if PinkSkyState._initialized:
|
| 22 |
+
return
|
| 23 |
+
PinkSkyState._initialized = True
|
| 24 |
+
|
| 25 |
+
self.models: Dict[str, ModelConfig] = {}
|
| 26 |
+
self.roles: Dict[str, Role] = {}
|
| 27 |
+
self.conductors: Dict[str, Conductor] = {}
|
| 28 |
+
self.current_mode: str = "chat"
|
| 29 |
+
self.current_conductor: str = "default"
|
| 30 |
+
self.current_role: str = "universal"
|
| 31 |
+
self.current_model: str = "deepseek-v4-pro"
|
| 32 |
+
self.chat_history: List[Dict[str, str]] = []
|
| 33 |
+
self.skill_history: List[Dict[str, str]] = []
|
| 34 |
+
self.build_history: List[Dict[str, str]] = []
|
| 35 |
+
self.build_context: Dict[str, Any] = {
|
| 36 |
+
"spec": "", "agents": 3, "models_tier": "tier1",
|
| 37 |
+
"skills_count": 2, "files_count": 3, "role": "universal",
|
| 38 |
+
"strategy": "parallel", "use_interpreter": True,
|
| 39 |
+
"notifications": True, "internet_access": True
|
| 40 |
+
}
|
| 41 |
+
self.cancel_flag: bool = False
|
| 42 |
+
self.load_all()
|
| 43 |
+
|
| 44 |
+
def load_all(self):
|
| 45 |
+
self._load_models()
|
| 46 |
+
self._load_roles()
|
| 47 |
+
self._load_conductors()
|
| 48 |
+
self._load_history()
|
| 49 |
+
|
| 50 |
+
def _build_model_config(self, name: str, data: dict) -> ModelConfig:
|
| 51 |
+
return ModelConfig(
|
| 52 |
+
name=name, provider="openai", endpoint=data["endpoint"],
|
| 53 |
+
api_key_env="NVIDIA_API_KEY",
|
| 54 |
+
context_window=data.get("context_window", 32000),
|
| 55 |
+
max_tokens=data.get("max_tokens", 8000),
|
| 56 |
+
cost_per_1k_input=data.get("cost_per_1k_input", 0.0),
|
| 57 |
+
cost_per_1k_output=data.get("cost_per_1k_output", 0.0),
|
| 58 |
+
coding_rank=data.get("coding_rank", 50),
|
| 59 |
+
speed_rank=data.get("speed_rank", 50),
|
| 60 |
+
reasoning_rank=data.get("reasoning_rank", 50),
|
| 61 |
+
tags=data.get("tags", [])
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
def _load_models(self):
|
| 65 |
+
defaults = {name: self._build_model_config(name, data) for name, data in MODEL_RANKING.items()}
|
| 66 |
+
if os.path.exists(MODELS_FILE):
|
| 67 |
+
try:
|
| 68 |
+
with open(MODELS_FILE, "r", encoding="utf-8") as f:
|
| 69 |
+
custom = json.load(f)
|
| 70 |
+
for k, v in custom.items():
|
| 71 |
+
if k not in defaults:
|
| 72 |
+
defaults[k] = ModelConfig(**v)
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print(f"⚠️ Ошибка загрузки models.json: {e}")
|
| 75 |
+
self.models = defaults
|
| 76 |
+
|
| 77 |
+
def _load_roles(self):
|
| 78 |
+
defaults = {
|
| 79 |
+
"universal": Role(
|
| 80 |
+
name="universal",
|
| 81 |
+
prompt="You are PinkSky -- a universal AI assistant and autonomous developer. You help users with any tasks, scripts, theory, and project creation from scratch.",
|
| 82 |
+
description="Universal assistant for any tasks",
|
| 83 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "qwen3.5-397b"],
|
| 84 |
+
complexity="medium",
|
| 85 |
+
tags=["general"]
|
| 86 |
+
),
|
| 87 |
+
"guru": Role(
|
| 88 |
+
name="guru",
|
| 89 |
+
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.",
|
| 90 |
+
description="Guru programmer. Elegant code with deep explanations.",
|
| 91 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
|
| 92 |
+
complexity="high",
|
| 93 |
+
tags=["coding", "senior", "mentor", "python"]
|
| 94 |
+
),
|
| 95 |
+
"hacker": Role(
|
| 96 |
+
name="hacker",
|
| 97 |
+
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.",
|
| 98 |
+
description="Hacker-coder. Optimization and unconventional solutions.",
|
| 99 |
+
preferred_models=["deepseek-v4-pro", "deepseek-v4-flash", "llama-4-maverick", "nemotron-super-49b"],
|
| 100 |
+
complexity="high",
|
| 101 |
+
tags=["coding", "optimization", "hacks", "performance"]
|
| 102 |
+
),
|
| 103 |
+
"architect": Role(
|
| 104 |
+
name="architect",
|
| 105 |
+
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.",
|
| 106 |
+
description="Software Architect. High-level system design.",
|
| 107 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "nemotron-3-super", "qwen3.5-397b"],
|
| 108 |
+
complexity="high",
|
| 109 |
+
tags=["architecture", "design", "system", "ddd"]
|
| 110 |
+
),
|
| 111 |
+
"principal": Role(
|
| 112 |
+
name="principal",
|
| 113 |
+
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.",
|
| 114 |
+
description="Principal engineer. Strategy, mentorship, hard problems.",
|
| 115 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
|
| 116 |
+
complexity="high",
|
| 117 |
+
tags=["leadership", "strategy", "mentoring", "legacy"]
|
| 118 |
+
),
|
| 119 |
+
"evangelist": Role(
|
| 120 |
+
name="evangelist",
|
| 121 |
+
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.",
|
| 122 |
+
description="Quality evangelist. Testing and quality culture.",
|
| 123 |
+
preferred_models=["kimi-k2.6", "deepseek-v4-pro", "mistral-medium-3.5"],
|
| 124 |
+
complexity="high",
|
| 125 |
+
tags=["quality", "testing", "tdd", "ci-cd"]
|
| 126 |
+
),
|
| 127 |
+
"techlead": Role(
|
| 128 |
+
name="techlead",
|
| 129 |
+
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.",
|
| 130 |
+
description="Tech Lead. Code review and team direction.",
|
| 131 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
|
| 132 |
+
complexity="high",
|
| 133 |
+
tags=["review", "leadership", "team", "mentoring"]
|
| 134 |
+
),
|
| 135 |
+
"qa": Role(
|
| 136 |
+
name="qa",
|
| 137 |
+
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.",
|
| 138 |
+
description="QA engineer. Bug hunting and test strategy.",
|
| 139 |
+
preferred_models=["mistral-small-4", "step-3.7-flash", "llama-3.3-70b", "deepseek-v4-flash"],
|
| 140 |
+
complexity="medium",
|
| 141 |
+
tags=["qa", "testing", "automation", "manual"]
|
| 142 |
+
),
|
| 143 |
+
"sdet": Role(
|
| 144 |
+
name="sdet",
|
| 145 |
+
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.",
|
| 146 |
+
description="SDET. Autotests and test infrastructure at dev level.",
|
| 147 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "llama-4-maverick", "mistral-medium-3.5"],
|
| 148 |
+
complexity="high",
|
| 149 |
+
tags=["sdet", "automation", "framework", "infrastructure"]
|
| 150 |
+
),
|
| 151 |
+
"qe": Role(
|
| 152 |
+
name="qe",
|
| 153 |
+
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.",
|
| 154 |
+
description="Quality engineer. Processes, metrics, and quality culture.",
|
| 155 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "nemotron-3-super"],
|
| 156 |
+
complexity="high",
|
| 157 |
+
tags=["qe", "process", "metrics", "culture", "sdlc"]
|
| 158 |
+
),
|
| 159 |
+
"researcher": Role(
|
| 160 |
+
name="researcher",
|
| 161 |
+
prompt="You are Researcher PinkSky. Deep topic analysis. Compare approaches: trade-offs, limitations. Structure: executive summary -> details -> sources. Identify trends. Evidence > opinions. Numbers > words.",
|
| 162 |
+
description="Researcher and analyst. Deep topic analysis.",
|
| 163 |
+
preferred_models=["deepseek-v4-pro", "qwen3.5-397b", "kimi-k2.6", "gpt-oss-120b"],
|
| 164 |
+
complexity="high",
|
| 165 |
+
tags=["research", "analysis", "comparison"]
|
| 166 |
+
),
|
| 167 |
+
"critic": Role(
|
| 168 |
+
name="critic",
|
| 169 |
+
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.",
|
| 170 |
+
description="Critic and auditor. Bug and issue hunting.",
|
| 171 |
+
preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
|
| 172 |
+
complexity="medium",
|
| 173 |
+
tags=["audit", "security", "review", "critic"]
|
| 174 |
+
),
|
| 175 |
+
}
|
| 176 |
+
if os.path.exists(ROLES_FILE):
|
| 177 |
+
try:
|
| 178 |
+
with open(ROLES_FILE, "r", encoding="utf-8") as f:
|
| 179 |
+
custom = json.load(f)
|
| 180 |
+
for k, v in custom.items():
|
| 181 |
+
if k not in defaults:
|
| 182 |
+
defaults[k] = Role(**v)
|
| 183 |
+
except Exception as e:
|
| 184 |
+
print(f"⚠️ Ошибка загрузки roles.json: {e}")
|
| 185 |
+
self.roles = defaults
|
| 186 |
+
|
| 187 |
+
def _load_conductors(self):
|
| 188 |
+
defaults = {
|
| 189 |
+
"default": Conductor(
|
| 190 |
+
name="default",
|
| 191 |
+
prompt="""You are Conductor PinkSky (Default). Analyze request and choose optimal roles and models.
|
| 192 |
+
|
| 193 |
+
RULES:
|
| 194 |
+
1. Simple questions -- 1 role, 1 model.
|
| 195 |
+
2. Complex tasks -- decompose, assign roles.
|
| 196 |
+
3. Consider cost: cheap for simple, powerful for complex.
|
| 197 |
+
4. If code -- add critic.
|
| 198 |
+
5. If architecture -- add architect.
|
| 199 |
+
|
| 200 |
+
AVAILABLE ROLES: guru, hacker, architect, principal, evangelist, techlead, qa, sdet, qe, researcher, critic, universal.
|
| 201 |
+
|
| 202 |
+
AVAILABLE MODELS (by coding rank, best to worst):
|
| 203 |
+
TIER 1 (Elite): deepseek-v4-pro, kimi-k2.6, qwen3.5-397b, mistral-large-3, gpt-oss-120b
|
| 204 |
+
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
|
| 205 |
+
TIER 3 (Good): step-3.7-flash, mistral-small-4, minimax-m2.7, nemotron-super-49b-v1, llama-3.2-90b-vision
|
| 206 |
+
TIER 4 (Fast): nemotron-nano-12b, nemotron-3-nano-30b, nemotron-nano-9b, nemotron-content-safety
|
| 207 |
+
TIER 5 (Specialized): nemotron-3-nano-omni, diffusiongemma
|
| 208 |
+
|
| 209 |
+
FORMAT (STRICT JSON):
|
| 210 |
+
{"strategy": "single|sequential|parallel", "tasks": [{"role": "role_name", "model": "model_name", "prompt": "subtask"}], "synthesis_prompt": "how to combine"}""",
|
| 211 |
+
description="Standard conductor -- balance of quality and speed",
|
| 212 |
+
strategy="selective",
|
| 213 |
+
max_agents=3,
|
| 214 |
+
cost_aware=True,
|
| 215 |
+
auto_rank_by="balanced"
|
| 216 |
+
),
|
| 217 |
+
"strict": Conductor(
|
| 218 |
+
name="strict",
|
| 219 |
+
prompt="""You are Strict Conductor PinkSky. Minimum agents, maximum efficiency.
|
| 220 |
+
|
| 221 |
+
RULES:
|
| 222 |
+
1. ONLY one role and one model.
|
| 223 |
+
2. Cheapest model capable of solving the task.
|
| 224 |
+
3. Only sequential.
|
| 225 |
+
|
| 226 |
+
FORMAT (STRICT JSON):
|
| 227 |
+
{"strategy": "single", "tasks": [{"role": "name", "model": "name", "prompt": "task"}], "synthesis_prompt": ""}""",
|
| 228 |
+
description="Minimum agents, minimum cost",
|
| 229 |
+
strategy="single",
|
| 230 |
+
max_agents=1,
|
| 231 |
+
cost_aware=True,
|
| 232 |
+
auto_rank_by="coding"
|
| 233 |
+
),
|
| 234 |
+
"creative": Conductor(
|
| 235 |
+
name="creative",
|
| 236 |
+
prompt="""You are Creative Conductor PinkSky. Maximum perspectives, brainstorm.
|
| 237 |
+
|
| 238 |
+
RULES:
|
| 239 |
+
1. Multiple roles from different angles.
|
| 240 |
+
2. Parallel strategy.
|
| 241 |
+
3. guru + hacker + researcher + critic.
|
| 242 |
+
4. Do not save on models -- use the best.
|
| 243 |
+
|
| 244 |
+
FORMAT (STRICT JSON):
|
| 245 |
+
{"strategy": "parallel", "tasks": [...], "synthesis_prompt": "synthesize creative ideas"}""",
|
| 246 |
+
description="Maximum roles, creative brainstorm",
|
| 247 |
+
strategy="parallel",
|
| 248 |
+
max_agents=5,
|
| 249 |
+
cost_aware=False,
|
| 250 |
+
auto_rank_by="coding"
|
| 251 |
+
),
|
| 252 |
+
"economy": Conductor(
|
| 253 |
+
name="economy",
|
| 254 |
+
prompt="""You are Economy Conductor PinkSky. Solve task for minimum cost.
|
| 255 |
+
|
| 256 |
+
RULES:
|
| 257 |
+
1. Start with TIER 4 (fast/cheap): nemotron-nano-9b, nemotron-nano-12b, nemotron-3-nano-30b.
|
| 258 |
+
2. Only if it fails -- escalate to TIER 3/2.
|
| 259 |
+
3. One role, one model.
|
| 260 |
+
|
| 261 |
+
FORMAT (STRICT JSON):
|
| 262 |
+
{"strategy": "single", "tasks": [{"role": "name", "model": "name", "prompt": "task"}], "synthesis_prompt": ""}""",
|
| 263 |
+
description="Cheap models, budget saving",
|
| 264 |
+
strategy="single",
|
| 265 |
+
max_agents=1,
|
| 266 |
+
cost_aware=True,
|
| 267 |
+
auto_rank_by="speed"
|
| 268 |
+
),
|
| 269 |
+
"review": Conductor(
|
| 270 |
+
name="review",
|
| 271 |
+
prompt="""You are Code Review Conductor PinkSky. Maximum quality code review.
|
| 272 |
+
|
| 273 |
+
RULES:
|
| 274 |
+
1. techlead (architectural review) + critic (bugs/vulnerabilities) + guru (best practices).
|
| 275 |
+
2. Parallel review.
|
| 276 |
+
3. Synthesize into structured report.
|
| 277 |
+
|
| 278 |
+
FORMAT (STRICT JSON):
|
| 279 |
+
{"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"}""",
|
| 280 |
+
description="Focus on code review. Multi-angle code check.",
|
| 281 |
+
strategy="parallel",
|
| 282 |
+
max_agents=4,
|
| 283 |
+
cost_aware=True,
|
| 284 |
+
auto_rank_by="coding"
|
| 285 |
+
),
|
| 286 |
+
"build": Conductor(
|
| 287 |
+
name="build",
|
| 288 |
+
prompt="""You are Project Build Conductor PinkSky. Build full project from spec.
|
| 289 |
+
|
| 290 |
+
RULES:
|
| 291 |
+
1. Sequential: architect -> guru/hacker -> sdet -> critic.
|
| 292 |
+
2. Each stage -- separate call.
|
| 293 |
+
|
| 294 |
+
FORMAT (STRICT JSON):
|
| 295 |
+
{"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"}""",
|
| 296 |
+
description="Project build. Architecture -> code -> tests -> audit.",
|
| 297 |
+
strategy="sequential",
|
| 298 |
+
max_agents=5,
|
| 299 |
+
cost_aware=True,
|
| 300 |
+
auto_rank_by="coding"
|
| 301 |
+
),
|
| 302 |
+
}
|
| 303 |
+
if os.path.exists(CONDUCTORS_FILE):
|
| 304 |
+
try:
|
| 305 |
+
with open(CONDUCTORS_FILE, "r", encoding="utf-8") as f:
|
| 306 |
+
custom = json.load(f)
|
| 307 |
+
for k, v in custom.items():
|
| 308 |
+
if k not in defaults:
|
| 309 |
+
defaults[k] = Conductor(**v)
|
| 310 |
+
except Exception as e:
|
| 311 |
+
print(f"⚠️ Ошибка загрузки conductors.json: {e}")
|
| 312 |
+
self.conductors = defaults
|
| 313 |
+
|
| 314 |
+
def _load_history(self):
|
| 315 |
+
if os.path.exists(HISTORY_FILE):
|
| 316 |
+
try:
|
| 317 |
+
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
|
| 318 |
+
data = json.load(f)
|
| 319 |
+
self.chat_history = data.get("chat", [])
|
| 320 |
+
self.skill_history = data.get("skill", [])
|
| 321 |
+
self.build_history = data.get("build", [])
|
| 322 |
+
except Exception as e:
|
| 323 |
+
print(f"⚠️ Ошибка загрузки истории: {e}")
|
| 324 |
+
|
| 325 |
+
def save_roles(self):
|
| 326 |
+
data = {k: {"name": v.name, "prompt": v.prompt, "description": v.description,
|
| 327 |
+
"preferred_models": v.preferred_models, "complexity": v.complexity, "tags": v.tags}
|
| 328 |
+
for k, v in self.roles.items()}
|
| 329 |
+
with open(ROLES_FILE, "w", encoding="utf-8") as f:
|
| 330 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 331 |
+
|
| 332 |
+
def save_models(self):
|
| 333 |
+
data = {k: {"name": v.name, "provider": v.provider, "endpoint": v.endpoint,
|
| 334 |
+
"api_key_env": v.api_key_env, "context_window": v.context_window,
|
| 335 |
+
"max_tokens": v.max_tokens, "cost_per_1k_input": v.cost_per_1k_input,
|
| 336 |
+
"cost_per_1k_output": v.cost_per_1k_output,
|
| 337 |
+
"coding_rank": v.coding_rank, "speed_rank": v.speed_rank, "reasoning_rank": v.reasoning_rank,
|
| 338 |
+
"tags": v.tags}
|
| 339 |
+
for k, v in self.models.items()}
|
| 340 |
+
with open(MODELS_FILE, "w", encoding="utf-8") as f:
|
| 341 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 342 |
+
|
| 343 |
+
def save_conductors(self):
|
| 344 |
+
data = {k: {"name": v.name, "prompt": v.prompt, "description": v.description,
|
| 345 |
+
"strategy": v.strategy, "max_agents": v.max_agents, "cost_aware": v.cost_aware,
|
| 346 |
+
"auto_rank_by": v.auto_rank_by}
|
| 347 |
+
for k, v in self.conductors.items()}
|
| 348 |
+
with open(CONDUCTORS_FILE, "w", encoding="utf-8") as f:
|
| 349 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 350 |
+
|
| 351 |
+
def save_history(self):
|
| 352 |
+
data = {"chat": self.chat_history, "skill": self.skill_history, "build": self.build_history}
|
| 353 |
+
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
|
| 354 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 355 |
+
|
| 356 |
+
def add_to_history(self, mode: str, role: str, content: str):
|
| 357 |
+
entry = {"role": role, "content": content, "timestamp": datetime.now().isoformat()}
|
| 358 |
+
if mode == "chat":
|
| 359 |
+
self.chat_history.append(entry)
|
| 360 |
+
elif mode == "skill":
|
| 361 |
+
self.skill_history.append(entry)
|
| 362 |
+
elif mode == "build":
|
| 363 |
+
self.build_history.append(entry)
|
| 364 |
+
self.save_history()
|
| 365 |
+
|
| 366 |
+
def get_best_model(self, rank_by: str = "coding", min_tier: int = 1, max_tier: int = 5, exclude: List[str] = None) -> str:
|
| 367 |
+
exclude = exclude or []
|
| 368 |
+
candidates = []
|
| 369 |
+
for name, model in self.models.items():
|
| 370 |
+
if name in exclude or name == "hf_fallback":
|
| 371 |
+
continue
|
| 372 |
+
tier = 5
|
| 373 |
+
if model.coding_rank <= 5: tier = 1
|
| 374 |
+
elif model.coding_rank <= 12: tier = 2
|
| 375 |
+
elif model.coding_rank <= 18: tier = 3
|
| 376 |
+
elif model.coding_rank <= 24: tier = 4
|
| 377 |
+
if min_tier <= tier <= max_tier:
|
| 378 |
+
candidates.append((name, model))
|
| 379 |
+
if not candidates:
|
| 380 |
+
return "deepseek-v4-pro"
|
| 381 |
+
if rank_by == "coding":
|
| 382 |
+
candidates.sort(key=lambda x: x[1].coding_rank)
|
| 383 |
+
elif rank_by == "speed":
|
| 384 |
+
candidates.sort(key=lambda x: x[1].speed_rank)
|
| 385 |
+
elif rank_by == "reasoning":
|
| 386 |
+
candidates.sort(key=lambda x: x[1].reasoning_rank)
|
| 387 |
+
elif rank_by == "balanced":
|
| 388 |
+
candidates.sort(key=lambda x: (x[1].coding_rank + x[1].speed_rank + x[1].reasoning_rank) / 3)
|
| 389 |
+
else:
|
| 390 |
+
candidates.sort(key=lambda x: x[1].coding_rank)
|
| 391 |
+
return candidates[0][0]
|
| 392 |
+
|
| 393 |
+
def get_model_for_role(self, role_name: str, preference: str = None, rank_by: str = None) -> str:
|
| 394 |
+
role = self.roles.get(role_name)
|
| 395 |
+
if not role:
|
| 396 |
+
return preference or self.current_model
|
| 397 |
+
conductor = self.conductors.get(self.current_conductor, self.conductors["default"])
|
| 398 |
+
rank_criteria = rank_by or conductor.auto_rank_by
|
| 399 |
+
max_tier = 5
|
| 400 |
+
if role.complexity == "high":
|
| 401 |
+
max_tier = 2
|
| 402 |
+
elif role.complexity == "medium":
|
| 403 |
+
max_tier = 3
|
| 404 |
+
if preference and preference in self.models:
|
| 405 |
+
return preference
|
| 406 |
+
available = [m for m in role.preferred_models if m in self.models and m != "hf_fallback"]
|
| 407 |
+
if available:
|
| 408 |
+
if conductor.cost_aware and rank_criteria != "coding":
|
| 409 |
+
available.sort(key=lambda m: self.models[m].cost_per_1k_output)
|
| 410 |
+
else:
|
| 411 |
+
if rank_criteria == "coding":
|
| 412 |
+
available.sort(key=lambda m: self.models[m].coding_rank)
|
| 413 |
+
elif rank_criteria == "speed":
|
| 414 |
+
available.sort(key=lambda m: self.models[m].speed_rank)
|
| 415 |
+
elif rank_criteria == "reasoning":
|
| 416 |
+
available.sort(key=lambda m: self.models[m].reasoning_rank)
|
| 417 |
+
else:
|
| 418 |
+
available.sort(key=lambda m: (self.models[m].coding_rank + self.models[m].speed_rank + self.models[m].reasoning_rank) / 3)
|
| 419 |
+
return available[0]
|
| 420 |
+
return self.get_best_model(rank_by=rank_criteria, max_tier=max_tier)
|
| 421 |
+
|
| 422 |
+
def get_models_by_tier(self, tier: int) -> List[str]:
|
| 423 |
+
result = []
|
| 424 |
+
for name, model in self.models.items():
|
| 425 |
+
if name == "hf_fallback":
|
| 426 |
+
continue
|
| 427 |
+
model_tier = 5
|
| 428 |
+
if model.coding_rank <= 5: model_tier = 1
|
| 429 |
+
elif model.coding_rank <= 12: model_tier = 2
|
| 430 |
+
elif model.coding_rank <= 18: model_tier = 3
|
| 431 |
+
elif model.coding_rank <= 24: model_tier = 4
|
| 432 |
+
if model_tier == tier:
|
| 433 |
+
result.append(name)
|
| 434 |
+
return result
|
| 435 |
+
|
| 436 |
+
def get_next_tier_model(self, current_model_name: str) -> Optional[str]:
|
| 437 |
+
if current_model_name not in self.models:
|
| 438 |
+
return None
|
| 439 |
+
current = self.models[current_model_name]
|
| 440 |
+
current_tier = 5
|
| 441 |
+
if current.coding_rank <= 5: current_tier = 1
|
| 442 |
+
elif current.coding_rank <= 12: current_tier = 2
|
| 443 |
+
elif current.coding_rank <= 18: current_tier = 3
|
| 444 |
+
elif current.coding_rank <= 24: current_tier = 4
|
| 445 |
+
next_tier = current_tier + 1
|
| 446 |
+
if next_tier > 5:
|
| 447 |
+
return None
|
| 448 |
+
models_in_tier = self.get_models_by_tier(next_tier)
|
| 449 |
+
if models_in_tier:
|
| 450 |
+
return models_in_tier[0]
|
| 451 |
+
return None
|
| 452 |
+
|
| 453 |
+
def export_history_json(self) -> str:
|
| 454 |
+
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)
|
| 455 |
+
|
| 456 |
+
def export_history_md(self) -> str:
|
| 457 |
+
lines = ["# PinkSky History Export", ""]
|
| 458 |
+
lines.append("*Exported: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "*")
|
| 459 |
+
lines.append("")
|
| 460 |
+
for mode, history in [("Chat", self.chat_history), ("Skill", self.skill_history), ("Build", self.build_history)]:
|
| 461 |
+
lines.append("## " + mode + " Mode")
|
| 462 |
+
lines.append("")
|
| 463 |
+
for entry in history:
|
| 464 |
+
ts = entry.get("timestamp", "unknown")
|
| 465 |
+
role = entry.get("role", "unknown")
|
| 466 |
+
content = entry.get("content", "")
|
| 467 |
+
lines.append("### " + role + " (" + ts + ")")
|
| 468 |
+
lines.append("")
|
| 469 |
+
lines.append("```")
|
| 470 |
+
lines.append(content[:500])
|
| 471 |
+
lines.append("```")
|
| 472 |
+
lines.append("")
|
| 473 |
+
return "\n".join(lines)
|
| 474 |
+
|
| 475 |
+
STATE = PinkSkyState()
|