FreshPixels commited on
Commit
8f22f55
·
verified ·
1 Parent(s): a0621c3

Upload 2 files

Browse files
Files changed (2) hide show
  1. notification_system.py +86 -0
  2. 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()