FreshPixels commited on
Commit
82aae3a
·
verified ·
1 Parent(s): 4160469

Upload state.py

Browse files
Files changed (1) hide show
  1. state.py +463 -0
state.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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", ""]
446
+ lines.append("*Exported: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "*")
447
+ lines.append("")
448
+ for mode, history in [("Chat", self.chat_history), ("Skill", self.skill_history), ("Build", self.build_history)]:
449
+ lines.append("## " + mode + " Mode")
450
+ lines.append("")
451
+ for entry in history:
452
+ ts = entry.get("timestamp", "unknown")
453
+ role = entry.get("role", "unknown")
454
+ content = entry.get("content", "")
455
+ lines.append("### " + role + " (" + ts + ")")
456
+ lines.append("")
457
+ lines.append("```")
458
+ lines.append(content[:500])
459
+ lines.append("```")
460
+ lines.append("")
461
+ return "\n".join(lines)
462
+
463
+ STATE = PinkSkyState()