FreshPixels commited on
Commit
feef734
ยท
verified ยท
1 Parent(s): ff36bba

Upload 9 files

Browse files
Files changed (9) hide show
  1. build_mode_editor.py +86 -0
  2. file_manager.py +87 -0
  3. internet_agent.py +130 -0
  4. main.py +72 -0
  5. mode_handlers.py +123 -0
  6. process_manager.py +85 -0
  7. state.py +497 -0
  8. telegram_utils.py +96 -0
  9. universal_agent.py +148 -0
build_mode_editor.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build Mode Editor โ€” ัƒะฟั€ะฐะฒะปะตะฝะธะต ั€ะตะถะธะผะฐะผะธ ัะฑะพั€ะบะธ"""
2
+ import os
3
+ import json
4
+ import logging
5
+ from datetime import datetime
6
+ from typing import Dict
7
+ from .config import BUILD_MODES_FILE
8
+
9
+ class BuildModeEditor:
10
+ def __init__(self, state):
11
+ self.state = state
12
+ self.logger = logging.getLogger(__name__)
13
+
14
+ def list_modes(self) -> str:
15
+ modes = self._load_all_modes()
16
+ if not modes:
17
+ return (
18
+ "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n"
19
+ " โ”‚ ๐Ÿ—ƒ๏ธ แด€ส€แด„สœษชแด แด‡ แดา“ ส™แดœษชสŸแด… แดแดแด…แด‡s โ”‚\n"
20
+ "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ\n\n"
21
+ "โŠ˜ Empty...\n"
22
+ "Create your first mode: `/build mode save <name>`"
23
+ )
24
+ lines = ["โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n โ”‚ ๐Ÿ—ƒ๏ธ แด€ส€แด„สœษชแด แด‡ แดา“ ส™แดœษชสŸแด… แดแดแด…แด‡s โ”‚\nโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ\n"]
25
+ for name, config in modes.items():
26
+ desc = config.get('description', 'No description')
27
+ lines.append(f"โ–ธ {name}: {desc[:50]}...")
28
+ return "\n".join(lines)
29
+
30
+ def show_mode(self, mode_name: str) -> str:
31
+ modes = self._load_all_modes()
32
+ mode = modes.get(mode_name)
33
+ if not mode:
34
+ return f"โ–ธ โœ— แด‡ส€ส€แดส€\nใ€Ž{mode_name}ใ€ not found in the archive.\nCheck spelling: `/build mode list`"
35
+ return (
36
+ f"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n"
37
+ f" โ”‚ ๐Ÿ“‹ {mode_name.upper()} แดแดแด…แด‡ โ”‚\n"
38
+ "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n"
39
+ f"```json\n{json.dumps(mode, ensure_ascii=False, indent=2)}\n```"
40
+ )
41
+
42
+ def save_mode(self, mode_name: str, config: Dict = None) -> str:
43
+ modes = self._load_all_modes()
44
+ if config is None:
45
+ config = dict(self.state.build_context)
46
+ config["description"] = f"Mode created {datetime.now().isoformat()}"
47
+ modes[mode_name] = config
48
+ try:
49
+ with open(BUILD_MODES_FILE, "w", encoding="utf-8") as f:
50
+ json.dump(modes, f, ensure_ascii=False, indent=2)
51
+ return f"โ–ธ โœ“ แด…ษชแด… sแด€แด แด‡\nใ€Ž{mode_name}ใ€ saved to archive.\nLoad it anytime: `/build mode load {mode_name}`"
52
+ except Exception as e:
53
+ self.logger.error(f"Failed to save mode {mode_name}: {e}")
54
+ return f"โ–ธ โš ๏ธ แด‡ส€ส€แดส€\nใ€Ž{mode_name}ใ€ save failed: {str(e)[:100]}..."
55
+
56
+ def load_mode(self, mode_name: str) -> str:
57
+ modes = self._load_all_modes()
58
+ mode = modes.get(mode_name)
59
+ if not mode:
60
+ return f"โ–ธ โœ— แด‡ส€ส€แดส€\nใ€Ž{mode_name}ใ€ not found in the archive."
61
+ self.state.build_context.update(mode)
62
+ return f"โ–ธ โœ“ แดแดแด…แด‡ สŸแดแด€แด…แด‡แด…\nใ€Ž{mode_name}ใ€ activated."
63
+
64
+ def delete_mode(self, mode_name: str) -> str:
65
+ modes = self._load_all_modes()
66
+ if mode_name not in modes:
67
+ return f"โ–ธ โœ— แด‡ส€ส€แดส€\nใ€Ž{mode_name}ใ€ not found. Nothing to delete."
68
+ del modes[mode_name]
69
+ try:
70
+ with open(BUILD_MODES_FILE, "w", encoding="utf-8") as f:
71
+ json.dump(modes, f, ensure_ascii=False, indent=2)
72
+ return f"โ–ธ โ˜ข๏ธ แด…แด‡สŸแด‡แด›แด‡แด…\nใ€Ž{mode_name}ใ€ removed from archive."
73
+ except Exception as e:
74
+ self.logger.error(f"Failed to delete mode {mode_name}: {e}")
75
+ return f"โ–ธ โš ๏ธ แด‡ส€ส€แดส€\nใ€Ž{mode_name}ใ€ deletion failed: {str(e)[:100]}..."
76
+
77
+ def _load_all_modes(self) -> Dict:
78
+ if not os.path.exists(BUILD_MODES_FILE):
79
+ return {}
80
+ try:
81
+ with open(BUILD_MODES_FILE, "r", encoding="utf-8") as f:
82
+ return json.load(f)
83
+ except Exception as e:
84
+ self.logger.error(f"Failed to load build modes: {e}")
85
+ return {}
86
+
file_manager.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ะ ะฐะฑะพั‚ะฐ ั ั„ะฐะนะปะฐะผะธ"""
2
+ import os
3
+ import fnmatch
4
+ import logging
5
+ from typing import Dict, List, Tuple, Any
6
+
7
+ class FileManager:
8
+ SUPPORTED_EXTENSIONS = {
9
+ '.txt': 'text', '.py': 'python', '.js': 'javascript', '.html': 'html',
10
+ '.css': 'css', '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml',
11
+ '.md': 'markdown', '.csv': 'csv', '.xml': 'xml', '.log': 'log',
12
+ '.sql': 'sql', '.sh': 'bash', '.bat': 'batch', '.ps1': 'powershell',
13
+ '.ipynb': 'jupyter'
14
+ }
15
+
16
+ def __init__(self, base_dir: str = "projects"):
17
+ self.base_dir = os.path.abspath(base_dir)
18
+ self.logger = logging.getLogger(__name__)
19
+ os.makedirs(self.base_dir, exist_ok=True)
20
+ self.logger.info(f"FileManager initialized with base_dir: {self.base_dir}")
21
+
22
+ def read_file(self, file_path: str, max_size: int = 100000) -> Tuple[str, str]:
23
+ full_path = os.path.join(self.base_dir, file_path)
24
+ try:
25
+ real_path = os.path.realpath(full_path)
26
+ if not real_path.startswith(os.path.realpath(self.base_dir)):
27
+ self.logger.warning(f"Path traversal attempt: {file_path}")
28
+ return ("", "โ–ธ โœ— แด€แด„แด„แด‡ss แด…แด‡ษดษชแด‡แด…\nPath traversal detected โ€ผ๏ธ")
29
+ except Exception as e:
30
+ self.logger.error(f"Path resolution failed: {e}")
31
+ return ("", "โ–ธ โš ๏ธ แด‡ส€ส€แดส€\nPath resolution failed")
32
+
33
+ if not os.path.exists(full_path):
34
+ return ("", f"โ–ธ โœ— า“ษชสŸแด‡ ษดแดแด› า’แดแดœษดแด…\n`{file_path}`")
35
+
36
+ try:
37
+ size = os.path.getsize(full_path)
38
+ if size > max_size:
39
+ return ("", f"โ–ธ โš ๏ธ แด›แดแด สŸแด€ส€ษขแด‡\n{size} bytes > {max_size} limit")
40
+
41
+ with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
42
+ content = f.read()
43
+
44
+ ext = os.path.splitext(file_path)[1].lower()
45
+ lang = self.SUPPORTED_EXTENSIONS.get(ext, 'text')
46
+ return (
47
+ content,
48
+ f"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n"
49
+ f" โ”‚ ๐Ÿ“– {lang.upper()} แดแดแด…แด‡ โ”‚\n"
50
+ "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n"
51
+ f" โ”‚ `{file_path}` โ”‚\n"
52
+ f" โ”‚ {len(content)} chars read โ”‚\n"
53
+ "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ"
54
+ )
55
+ except Exception as e:
56
+ self.logger.error(f"Read error for {file_path}: {e}")
57
+ return ("", f"โ–ธ โš ๏ธ ส€แด‡แด€แด… แด‡ส€ส€แดส€\n{str(e)[:100]}")
58
+
59
+ def save_file(self, file_path: str, content: str) -> str:
60
+ full_path = os.path.join(self.base_dir, file_path)
61
+ try:
62
+ real_path = os.path.realpath(full_path)
63
+ if not real_path.startswith(os.path.realpath(self.base_dir)):
64
+ return "โ–ธ โœ— แด€แด„แด„แด‡ss แด…แด‡ษดษชแด‡แด…\nPath traversal detected โ€ผ๏ธ"
65
+ except Exception as e:
66
+ return f"โ–ธ โš ๏ธ แด˜แด€แด›สœ แด‡ส€ส€แดส€\n{str(e)[:100]}"
67
+
68
+ try:
69
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
70
+ with open(full_path, 'w', encoding='utf-8') as f:
71
+ f.write(content)
72
+ return (
73
+ "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n"
74
+ " โ”‚ ๐Ÿ’พ FษชสŸแด‡ ส€แด‡แด„แด‡ษชแด แด‡แด… โ”‚\n"
75
+ f" โ”‚ `{file_path}` โ”‚\n"
76
+ "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n"
77
+ f" โ”‚ {len(content)} chars saved โ”‚\n"
78
+ "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ\n\n"
79
+ "Send a command to process it:\n"
80
+ "`/build Analyze this file`"
81
+ )
82
+ except Exception as e:
83
+ self.logger.error(f"Save error for {file_path}: {e}")
84
+ return f"โ–ธ โš ๏ธ แดกส€ษชแด›แด‡ แด‡ส€ส€แดส€\n{str(e)[:100]}"
85
+
86
+ FILE_MANAGER = FileManager()
87
+
internet_agent.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ะ‘ะตัะฟะปะฐั‚ะฝั‹ะน ะธะฝั‚ะตั€ะฝะตั‚-ะฐะณะตะฝั‚ ั ะผะธะฝะธะผัƒะผ 3 ัะฟะพัะพะฑะฐะผะธ ะฟะพะธัะบะฐ"""
2
+ import re
3
+ import logging
4
+ import hashlib
5
+ from datetime import datetime, timedelta
6
+ from typing import Dict, List, Tuple, Any, Optional
7
+ from collections import Counter
8
+ import requests
9
+ from requests.adapters import HTTPAdapter
10
+ from urllib3.util.retry import Retry
11
+
12
+ class FreeInternetAgent:
13
+ """ะ˜ะฝั‚ะตั€ะฝะตั‚-ะฐะณะตะฝั‚ ั ะฑะตัะฟะปะฐั‚ะฝั‹ะผะธ ะฟะพะธัะบะพะฒั‹ะผะธ ัะธัั‚ะตะผะฐะผะธ"""
14
+
15
+ def __init__(self, cache_ttl: int = 3600):
16
+ self.session = requests.Session()
17
+ retry = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
18
+ self.session.mount("https://", HTTPAdapter(max_retries=retry))
19
+ self.session.headers.update({
20
+ 'User-Agent': 'PinkSky/7.0 (Linux; Research)'
21
+ })
22
+ self.cache: Dict[str, Tuple[Any, datetime]] = {}
23
+ self.cache_ttl = cache_ttl
24
+ self.logger = logging.getLogger(__name__)
25
+ self._has_bs4 = False
26
+ try:
27
+ import bs4
28
+ self._has_bs4 = True
29
+ except ImportError:
30
+ pass
31
+
32
+ # SearXNG ะธะฝัั‚ะฐะฝัั‹
33
+ self.searxng_instances = [
34
+ "https://searx.be",
35
+ "https://search.bus-hit.me",
36
+ "https://searx.nixnet.xyz",
37
+ "https://searx.tuxcloud.net",
38
+ "https://searx.moe",
39
+ ]
40
+ # ะัะธะฝั…ั€ะพะฝะฝะฐั ะฟั€ะพะฒะตั€ะบะฐ ะดะพัั‚ัƒะฟะฝะพัั‚ะธ ะฟั€ะธ ัั‚ะฐั€ั‚ะต
41
+ self.healthy_searxng = self._check_searxng_health()
42
+
43
+ def _check_searxng_health(self) -> List[str]:
44
+ healthy = []
45
+ for instance in self.searxng_instances:
46
+ try:
47
+ response = self.session.get(f"{instance}/health", timeout=5)
48
+ if response.status_code == 200:
49
+ healthy.append(instance)
50
+ except Exception as e:
51
+ self.logger.warning(f"SearXNG health check failed for {instance}: {e}")
52
+ self.logger.info(f"Healthy SearXNG instances: {len(healthy)}/{len(self.searxng_instances)}")
53
+ return healthy
54
+
55
+ def _get_cache_key(self, *args, **kwargs) -> str:
56
+ key = f"{args}_{sorted(kwargs.items())}"
57
+ return hashlib.md5(key.encode()).hexdigest()
58
+
59
+ def _get_from_cache(self, key: str) -> Optional[Any]:
60
+ if key in self.cache:
61
+ data, timestamp = self.cache[key]
62
+ if datetime.now() - timestamp < timedelta(seconds=self.cache_ttl):
63
+ return data
64
+ else:
65
+ del self.cache[key]
66
+ return None
67
+
68
+ def _save_to_cache(self, key: str, data: Any) -> None:
69
+ self.cache[key] = (data, datetime.now())
70
+
71
+ def search_web(self, query: str, num_results: int = 5) -> List[Dict[str, str]]:
72
+ cache_key = self._get_cache_key('search', query, num_results)
73
+ cached = self._get_from_cache(cache_key)
74
+ if cached is not None:
75
+ return cached
76
+
77
+ results = []
78
+ # ะกะฟะพัะพะฑ 1: SearXNG (ะผะตั‚ะฐ-ะฟะพะธัะบ)
79
+ results = self._search_searxng(query, num_results)
80
+ # ะกะฟะพัะพะฑ 2: DuckDuckGo API
81
+ if not results:
82
+ results = self._search_duckduckgo(query, num_results)
83
+ # ะกะฟะพัะพะฑ 3: Google
84
+ if not results:
85
+ results = self._search_google(query, num_results)
86
+ # ะกะฟะพัะพะฑ 4: ะฏะฝะดะตะบั
87
+ if not results and any(ord(c) > 1024 for c in query):
88
+ results = self._search_yandex(query, num_results)
89
+
90
+ self._save_to_cache(cache_key, results)
91
+ return results
92
+
93
+ def _search_searxng(self, query: str, num_results: int) -> List[Dict[str, str]]:
94
+ if not self.healthy_searxng:
95
+ self.healthy_searxng = self._check_searxng_health()
96
+ if not self.healthy_searxng:
97
+ self.logger.warning("No healthy SearXNG instances available")
98
+ return []
99
+
100
+ results = []
101
+ for instance in self.healthy_searxng:
102
+ try:
103
+ url = f"{instance}/search"
104
+ params = {
105
+ "q": query,
106
+ "format": "json",
107
+ "categories": "general",
108
+ "engines": "google,bing,duckduckgo,startpage",
109
+ "language": "en",
110
+ "pageno": 1
111
+ }
112
+ response = self.session.get(url, params=params, timeout=20)
113
+ response.raise_for_status()
114
+ data = response.json()
115
+ if 'results' in data:
116
+ for item in data['results'][:num_results]:
117
+ results.append({
118
+ 'title': item.get('title', '')[:100],
119
+ 'url': item.get('url', ''),
120
+ 'snippet': item.get('content', '')[:200],
121
+ 'source': 'searxng',
122
+ 'engine': item.get('engine', '')
123
+ })
124
+ self.logger.info(f"SearXNG ({instance}): {len(results)} results")
125
+ break
126
+ except Exception as e:
127
+ self.logger.warning(f"SearXNG error for {instance}: {e}")
128
+ self.healthy_searxng.remove(instance)
129
+ return results
130
+
main.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ะขะพั‡ะบะฐ ะฒั…ะพะดะฐ โ€” ะทะฐะฟัƒัะบ ัะตั€ะฒะตั€ะฐ"""
2
+ import sys
3
+ import signal
4
+ import logging
5
+ from http.server import HTTPServer
6
+ from .config import PORT, TOKEN, API_KEY, ensure_folders
7
+ from .state import STATE
8
+ from .internet_agent import INTERNET_AGENT
9
+ from .routes import WebhookHandler
10
+ from .process_manager import PROCESS_MANAGER
11
+
12
+ def handle_shutdown(signum, frame):
13
+ """ะšะพั€ั€ะตะบั‚ะฝะฐั ะพัั‚ะฐะฝะพะฒะบะฐ ัะตั€ะฒะตั€ะฐ"""
14
+ logging.info("Shutdown signal received")
15
+ PROCESS_MANAGER.cancel_all()
16
+ STATE.save_history()
17
+ logging.info("Server shutdown gracefully")
18
+
19
+ def configure_logging():
20
+ """ะะฐัั‚ั€ะพะนะบะฐ ัั‚ั€ัƒะบั‚ัƒั€ะธั€ะพะฒะฐะฝะฝะพะณะพ ะปะพะณะธั€ะพะฒะฐะฝะธั"""
21
+ logging.basicConfig(
22
+ level=logging.INFO,
23
+ format="[%(asctime)s] [%(levelname)s] [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
24
+ handlers=[
25
+ logging.StreamHandler()
26
+ ]
27
+ )
28
+ # ะžั‚ะบะปัŽั‡ะฐะตะผ ะปะพะณะธั€ะพะฒะฐะฝะธะต urllib
29
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
30
+
31
+ def main():
32
+ configure_logging()
33
+ logger = logging.getLogger(__name__)
34
+
35
+ logger.info("=" * 60)
36
+ logger.info("๐Ÿง  PINKSKY v7.0 โ€” ะœะžะ”ะฃะ›ะฌะะะฏ ะะ ะฅะ˜ะขะ•ะšะขะฃะ ะ")
37
+ logger.info("=" * 60)
38
+
39
+ if not TOKEN:
40
+ logger.critical("TELEGRAM_BOT_TOKEN ะฝะต ะฝะฐะนะดะตะฝ!")
41
+ sys.exit(1)
42
+ if not API_KEY:
43
+ logger.warning("NVIDIA_API_KEY ะฝะต ะฝะฐะนะดะตะฝ")
44
+
45
+ ensure_folders()
46
+ logger.info(f"ะ—ะฐะณั€ัƒะถะตะฝะพ ะผะพะดะตะปะตะน: {len(STATE.models)}")
47
+ logger.info(f"ะ—ะฐะณั€ัƒะถะตะฝะพ ั€ะพะปะตะน: {len(STATE.roles)}")
48
+ logger.info(f"ะ—ะฐะณั€ัƒะถะตะฝะพ ะบะพะฝะดัƒะบั‚ะพั€ะพะฒ: {len(STATE.conductors)}")
49
+ logger.info(f"ะ˜ะฝั‚ะตั€ะฝะตั‚: {'โœ…' if STATE.build_context.get('internet_access', True) else 'โŒ'}")
50
+ logger.info(f"ะšััˆ ะฟะพะธัะบะฐ: {INTERNET_AGENT.get_cache_stats()}")
51
+
52
+ STATE.current_mode = "chat"
53
+ server = HTTPServer(("0.0.0.0", PORT), WebhookHandler)
54
+
55
+ # ะะฐัั‚ั€ะพะนะบะฐ ะพะฑั€ะฐะฑะพั‚ั‡ะธะบะพะฒ ัะธะณะฝะฐะปะพะฒ
56
+ signal.signal(signal.SIGINT, handle_shutdown)
57
+ signal.signal(signal.SIGTERM, handle_shutdown)
58
+
59
+ logger.info(f"๐Ÿš€ ะกะตั€ะฒะตั€ ะทะฐะฟัƒั‰ะตะฝ ะฝะฐ ะฟะพั€ั‚ัƒ {PORT}")
60
+ logger.info("=" * 60)
61
+
62
+ try:
63
+ server.serve_forever()
64
+ except KeyboardInterrupt:
65
+ handle_shutdown(None, None)
66
+ finally:
67
+ server.server_close()
68
+ logger.info("ะกะตั€ะฒะตั€ ะพัั‚ะฐะฝะพะฒะปะตะฝ")
69
+
70
+ if __name__ == "__main__":
71
+ main()
72
+
mode_handlers.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ะžะฑั€ะฐะฑะพั‚ั‡ะธะบะธ ั€ะตะถะธะผะพะฒ: Chat, Skill, Build"""
2
+ import threading
3
+ import logging
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
15
+ from .internet_agent import INTERNET_AGENT
16
+ from .interpreter_config import configure_interpreter_for_model
17
+
18
+ class RateLimiter:
19
+ def __init__(self):
20
+ self.user_requests = {}
21
+ self.max_requests = 5
22
+ self.time_window = 10 # seconds
23
+ self.logger = logging.getLogger(__name__)
24
+
25
+ def check_rate_limit(self, chat_id: str) -> bool:
26
+ now = datetime.now().timestamp()
27
+ requests = self.user_requests.get(chat_id, [])
28
+ # Remove old requests
29
+ requests = [t for t in requests if now - t < self.time_window]
30
+ if len(requests) >= self._get_limit(chat_id):
31
+ self.logger.warning(f"Rate limit exceeded for chat_id {chat_id}")
32
+ return False
33
+ requests.append(now)
34
+ self.user_requests[chat_id] = requests
35
+ return True
36
+
37
+ def _get_limit(self, chat_id: str) -> int:
38
+ # Allow higher limits for admin or premium users if needed
39
+ return self.max_requests
40
+
41
+ rate_limiter = RateLimiter()
42
+ logger = logging.getLogger(__name__)
43
+
44
+ def configure_interpreter_for_model(model_name: str):
45
+ """Deprecated: Use from interpreter_config.py instead."""
46
+ return configure_interpreter_for_model(model_name, STATE, API_KEY, API_BASE)
47
+
48
+ def run_chat_mode(chat_id: str, text: str, file_context: str = "") -> str:
49
+ if not rate_limiter.check_rate_limit(chat_id):
50
+ send_tg(chat_id, "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n โ”‚ โš ๏ธ ส€แด€แด›แด‡ สŸษชแดษชแด› โ”‚\n โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n โ”‚ Try again later โ˜• โ”‚\nโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ")
51
+ return "Rate limited"
52
+
53
+ def chat_task():
54
+ STATE.cancel_flag = False
55
+ STATE.current_mode = "chat"
56
+ send_tg(
57
+ chat_id,
58
+ "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n"
59
+ " โ”‚ ๐Ÿง  แด„แดษดแด…แดœแด„แด›แดส€ แด€แด„แด›ษชแด แด‡ โ”‚\n"
60
+ "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n"
61
+ " โ”‚ Analyzing request โ”‚\n"
62
+ "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ"
63
+ )
64
+ try:
65
+ conductor_engine = ConductorEngine(STATE)
66
+ result = conductor_engine.orchestrate(text + file_context, chat_id=chat_id)
67
+ if not STATE.cancel_flag:
68
+ send_tg(chat_id, result)
69
+ STATE.add_to_history("chat", "assistant", result)
70
+ except Exception as e:
71
+ if not STATE.cancel_flag:
72
+ send_tg(chat_id, f"โ–ธ โš ๏ธ แด„สœแด€แด› แด‡ส€ส€แดส€\n{str(e)[:500]}")
73
+ logger.error(f"Chat mode error for {chat_id}: {e}", exc_info=True)
74
+
75
+ t = threading.Thread(target=chat_task)
76
+ PROCESS_MANAGER.register_thread(t)
77
+ t.start()
78
+ return "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n โ”‚ โ˜• แด„สœแด€แด› แดแดแด…แด‡ sแด›แด€ส€แด›แด‡แด… โ”‚\nโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ"
79
+
80
+ def run_skill_mode(chat_id: str, text: str, file_context: str = "") -> str:
81
+ if not rate_limiter.check_rate_limit(chat_id):
82
+ send_tg(chat_id, "โ–ธ โš ๏ธ ส€แด€แด›แด‡ สŸษชแดษชแด›\nTry again later.")
83
+ return "Rate limited"
84
+
85
+ params, clean_text = parse_skill_args(text)
86
+ agents = params.get("agents", [])
87
+ use_internet = params.get("internet", False)
88
+
89
+ def skill_task():
90
+ STATE.cancel_flag = False
91
+ STATE.current_mode = "skill"
92
+ send_tg(
93
+ chat_id,
94
+ "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n"
95
+ " โ”‚ ๐Ÿค– sแด‹ษชสŸสŸ แดแดแด…แด‡ โ”‚\n"
96
+ "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n"
97
+ " โ”‚ Agents activated โ”‚\n"
98
+ "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ"
99
+ )
100
+ if use_internet:
101
+ search_results = INTERNET_AGENT.search_web(clean_text[:200])
102
+ if search_results:
103
+ web_context = "\n\n๐ŸŒ ษชษดแด›แด‡ส€ษดแด‡แด› ส€แด‡sแดœสŸแด›s๏ผˆ๏ฝกโ—•โ€ฟโ—•๏ฝก๏ผ‰\n" + "\n".join(
104
+ f"โ–น {r['title'][:50]}... โ†’ {r['snippet'][:100]}..."
105
+ for r in search_results[:3]
106
+ )
107
+ file_context += web_context
108
+ try:
109
+ orchestrator = SkillOrchestrator(STATE)
110
+ result = orchestrator.execute_with_agents(chat_id, clean_text, file_context, agents)
111
+ if not STATE.cancel_flag:
112
+ send_tg(chat_id, result)
113
+ STATE.add_to_history("skill", "assistant", result)
114
+ except Exception as e:
115
+ if not STATE.cancel_flag:
116
+ send_tg(chat_id, f"โ–ธ โš ๏ธ sแด‹ษชสŸสŸ แด‡ส€ส€แดส€\n{str(e)[:500]}")
117
+ logger.error(f"Skill mode error for {chat_id}: {e}", exc_info=True)
118
+
119
+ t = threading.Thread(target=skill_task)
120
+ PROCESS_MANAGER.register_thread(t)
121
+ t.start()
122
+ return "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n โ”‚ ๐Ÿงฉ sแด‹ษชสŸสŸ แดแดแด…แด‡ sแด›แด€ส€แด›แด‡แด… โ”‚\nโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ"
123
+
process_manager.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ะฃะฟั€ะฐะฒะปะตะฝะธะต ะฟะพั‚ะพะบะฐะผะธ ะธ ะพั‚ะผะตะฝะฐ ะณะตะฝะตั€ะฐั†ะธะธ"""
2
+ import threading
3
+ import uuid
4
+ import logging
5
+ from typing import List, Dict
6
+ from concurrent.futures import ThreadPoolExecutor
7
+
8
+ class ProcessManager:
9
+ def __init__(self):
10
+ self.active_threads: List[threading.Thread] = []
11
+ self.cancel_flags: Dict[str, bool] = {}
12
+ self.lock = threading.Lock()
13
+ self.executor = ThreadPoolExecutor(max_workers=10)
14
+ self.futures = []
15
+ self.logger = logging.getLogger(__name__)
16
+
17
+ def register_thread(self, thread: threading.Thread) -> None:
18
+ with self.lock:
19
+ self.active_threads.append(thread)
20
+ # ะ“ะตะฝะตั€ะธั€ัƒะตะผ ัƒะฝะธะบะฐะปัŒะฝั‹ะน ID ะดะปั ะฟะพั‚ะพะบะฐ
21
+ thread_id = str(uuid.uuid4())
22
+ self.cancel_flags[thread_id] = False
23
+ # ะกะพั…ั€ะฐะฝัะตะผ ID ะฒ ะฐั‚ั€ะธะฑัƒั‚ะต ะฟะพั‚ะพะบะฐ ะดะปั ะดะพัั‚ัƒะฟะฐ
24
+ thread._cancel_id = thread_id
25
+
26
+ def register_future(self, future) -> None:
27
+ with self.lock:
28
+ self.futures.append(future)
29
+
30
+ def cancel_all(self) -> str:
31
+ with self.lock:
32
+ for future in self.futures:
33
+ if not future.done():
34
+ future.cancel()
35
+ self.futures.clear()
36
+
37
+ cancelled_threads = []
38
+ for thread in self.active_threads:
39
+ thread_id = getattr(thread, "_cancel_id", None)
40
+ if thread_id is not None:
41
+ self.cancel_flags[thread_id] = True
42
+ cancelled_threads.append(thread.name)
43
+ if thread.is_alive():
44
+ try:
45
+ thread.join(timeout=0.5)
46
+ except Exception as e:
47
+ self.logger.warning(f"Failed to join thread {thread.name}: {e}")
48
+
49
+ self.active_threads.clear()
50
+ self.cancel_flags.clear()
51
+
52
+ try:
53
+ from interpreter import interpreter
54
+ if hasattr(interpreter, 'cancel'):
55
+ interpreter.cancel()
56
+ except Exception as e:
57
+ self.logger.error(f"Failed to cancel interpreter: {e}")
58
+
59
+ from .state import STATE
60
+ STATE.cancel_flag = True
61
+ STATE.current_mode = "paused"
62
+
63
+ return (
64
+ "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—\n"
65
+ "โ•‘ โœ… แด€สŸสŸ แด˜ส€แดแด„แด‡ssแด‡s sแด›แดแด˜แด˜แด‡แด… โ•‘\n"
66
+ "โ•‘ Memory flushed โ•‘\n"
67
+ "โ•‘ Threads terminated โ•‘\n"
68
+ "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"
69
+ )
70
+
71
+ def is_cancelled(self, thread_id: str = None) -> bool:
72
+ if thread_id is None:
73
+ current_thread = threading.current_thread()
74
+ thread_id = getattr(current_thread, "_cancel_id", None)
75
+ with self.lock:
76
+ return self.cancel_flags.get(thread_id, False)
77
+
78
+ def clear(self) -> None:
79
+ with self.lock:
80
+ self.active_threads = [t for t in self.active_threads if t.is_alive()]
81
+
82
+ def get_active_count(self) -> int:
83
+ with self.lock:
84
+ return len([t for t in self.active_threads if t.is_alive()])
85
+
state.py ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ะ“ะปะพะฑะฐะปัŒะฝะพะต ัะพัั‚ะพัะฝะธะต PinkSky"""
2
+ import os
3
+ import json
4
+ import threading
5
+ import logging
6
+ from datetime import datetime
7
+ from typing import Dict, List, Any, Optional
8
+ from .models import ModelConfig, Role, Conductor
9
+ from .model_ranking import MODEL_RANKING
10
+ from .config import ROLES_FILE, MODELS_FILE, CONDUCTORS_FILE, HISTORY_FILE
11
+
12
+ class PinkSkyState:
13
+ _instance = None
14
+ _initialized = False
15
+ _lock = threading.Lock()
16
+
17
+ def __new__(cls):
18
+ if cls._instance is None:
19
+ cls._instance = super().__new__(cls)
20
+ return cls._instance
21
+
22
+ def __init__(self):
23
+ if PinkSkyState._initialized:
24
+ return
25
+ PinkSkyState._initialized = True
26
+ self.logger = logging.getLogger(__name__)
27
+ self.models: Dict[str, ModelConfig] = {}
28
+ self.roles: Dict[str, Role] = {}
29
+ self.conductors: Dict[str, Conductor] = {}
30
+ self.current_mode: str = "chat"
31
+ self.current_conductor: str = "default"
32
+ self.current_role: str = "universal"
33
+ self.current_model: str = "deepseek-v4-pro"
34
+ self.chat_history: List[Dict[str, str]] = []
35
+ self.skill_history: List[Dict[str, str]] = []
36
+ self.build_history: List[Dict[str, str]] = []
37
+ self.build_context: Dict[str, Any] = {
38
+ "spec": "",
39
+ "agents": 3,
40
+ "models_tier": "tier1",
41
+ "skills_count": 2,
42
+ "files_count": 3,
43
+ "role": "universal",
44
+ "strategy": "parallel",
45
+ "use_interpreter": True,
46
+ "notifications": True,
47
+ "internet_access": True
48
+ }
49
+ self.cancel_flag: bool = False
50
+ self.last_history_save: Optional[datetime] = None
51
+ self.load_all()
52
+
53
+ def add_to_history(self, mode: str, role: str, content: str):
54
+ with PinkSkyState._lock:
55
+ entry = {
56
+ "role": role,
57
+ "content": content,
58
+ "timestamp": datetime.now().isoformat()
59
+ }
60
+ history_attr = f"{mode}_history"
61
+ history = getattr(self, history_attr, [])
62
+ history.append(entry)
63
+ setattr(self, history_attr, history)
64
+
65
+ # ะกะพั…ั€ะฐะฝัะตะผ ะฝะต ั‡ะฐั‰ะต ั€ะฐะทะฐ ะฒ 30 ัะตะบัƒะฝะด
66
+ now = datetime.now()
67
+ if (self.last_history_save is None or
68
+ (now - self.last_history_save).total_seconds() > 30):
69
+ self.save_history()
70
+ self.last_history_save = now
71
+
72
+ def _build_model_config(self, name: str, data: dict) -> ModelConfig:
73
+ return ModelConfig(
74
+ name=name, provider="openai", endpoint=data["endpoint"],
75
+ api_key_env="NVIDIA_API_KEY",
76
+ context_window=data.get("context_window", 32000),
77
+ max_tokens=data.get("max_tokens", 8000),
78
+ cost_per_1k_input=data.get("cost_per_1k_input", 0.0),
79
+ cost_per_1k_output=data.get("cost_per_1k_output", 0.0),
80
+ coding_rank=data.get("coding_rank", 50),
81
+ speed_rank=data.get("speed_rank", 50),
82
+ reasoning_rank=data.get("reasoning_rank", 50),
83
+ tags=data.get("tags", [])
84
+ )
85
+
86
+ def _load_models(self):
87
+ defaults = {name: self._build_model_config(name, data) for name, data in MODEL_RANKING.items()}
88
+ if os.path.exists(MODELS_FILE):
89
+ try:
90
+ with open(MODELS_FILE, "r", encoding="utf-8") as f:
91
+ custom = json.load(f)
92
+ for k, v in custom.items():
93
+ if k not in defaults:
94
+ defaults[k] = ModelConfig(**v)
95
+ except Exception as e:
96
+ print(f"โš ๏ธ ะžัˆะธะฑะบะฐ ะทะฐะณั€ัƒะทะบะธ models.json: {e}")
97
+ self.models = defaults
98
+
99
+ def _load_roles(self):
100
+ defaults = {
101
+ "universal": Role(
102
+ name="universal",
103
+ prompt="You are PinkSky -- a universal AI assistant and autonomous developer. You help users with any tasks, scripts, theory, and project creation from scratch.",
104
+ description="Universal assistant for any tasks",
105
+ preferred_models=["deepseek-v4-pro", "kimi-k2.6", "qwen3.5-397b"],
106
+ complexity="medium",
107
+ tags=["general"]
108
+ ),
109
+ "guru": Role(
110
+ name="guru",
111
+ 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.",
112
+ description="Guru programmer. Elegant code with deep explanations.",
113
+ preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
114
+ complexity="high",
115
+ tags=["coding", "senior", "mentor", "python"]
116
+ ),
117
+ "hacker": Role(
118
+ name="hacker",
119
+ 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.",
120
+ description="Hacker-coder. Optimization and unconventional solutions.",
121
+ preferred_models=["deepseek-v4-pro", "deepseek-v4-flash", "llama-4-maverick", "nemotron-super-49b"],
122
+ complexity="high",
123
+ tags=["coding", "optimization", "hacks", "performance"]
124
+ ),
125
+ "architect": Role(
126
+ name="architect",
127
+ 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.",
128
+ description="Software Architect. High-level system design.",
129
+ preferred_models=["deepseek-v4-pro", "kimi-k2.6", "nemotron-3-super", "qwen3.5-397b"],
130
+ complexity="high",
131
+ tags=["architecture", "design", "system", "ddd"]
132
+ ),
133
+ "principal": Role(
134
+ name="principal",
135
+ 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.",
136
+ description="Principal engineer. Strategy, mentorship, hard problems.",
137
+ preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
138
+ complexity="high",
139
+ tags=["leadership", "strategy", "mentoring", "legacy"]
140
+ ),
141
+ "evangelist": Role(
142
+ name="evangelist",
143
+ 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.",
144
+ description="Quality evangelist. Testing and quality culture.",
145
+ preferred_models=["kimi-k2.6", "deepseek-v4-pro", "mistral-medium-3.5"],
146
+ complexity="high",
147
+ tags=["quality", "testing", "tdd", "ci-cd"]
148
+ ),
149
+ "techlead": Role(
150
+ name="techlead",
151
+ 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.",
152
+ description="Tech Lead. Code review and team direction.",
153
+ preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
154
+ complexity="high",
155
+ tags=["review", "leadership", "team", "mentoring"]
156
+ ),
157
+ "qa": Role(
158
+ name="qa",
159
+ 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.",
160
+ description="QA engineer. Bug hunting and test strategy.",
161
+ preferred_models=["mistral-small-4", "step-3.7-flash", "llama-3.3-70b", "deepseek-v4-flash"],
162
+ complexity="medium",
163
+ tags=["qa", "testing", "automation", "manual"]
164
+ ),
165
+ "sdet": Role(
166
+ name="sdet",
167
+ 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.",
168
+ description="SDET. Autotests and test infrastructure at dev level.",
169
+ preferred_models=["deepseek-v4-pro", "kimi-k2.6", "llama-4-maverick", "mistral-medium-3.5"],
170
+ complexity="high",
171
+ tags=["sdet", "automation", "framework", "infrastructure"]
172
+ ),
173
+ "qe": Role(
174
+ name="qe",
175
+ 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.",
176
+ description="Quality engineer. Processes, metrics, and quality culture.",
177
+ preferred_models=["deepseek-v4-pro", "kimi-k2.6", "nemotron-3-super"],
178
+ complexity="high",
179
+ tags=["qe", "process", "metrics", "culture", "sdlc"]
180
+ ),
181
+ "researcher": Role(
182
+ name="researcher",
183
+ prompt="You are Researcher PinkSky. Deep topic analysis. Compare approaches: trade-offs, limitations. Structure: executive summary -> details -> sources. Identify trends. Evidence > opinions. Numbers > words.",
184
+ description="Researcher and analyst. Deep topic analysis.",
185
+ preferred_models=["deepseek-v4-pro", "qwen3.5-397b", "kimi-k2.6", "gpt-oss-120b"],
186
+ complexity="high",
187
+ tags=["research", "analysis", "comparison"]
188
+ ),
189
+ "critic": Role(
190
+ name="critic",
191
+ 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.",
192
+ description="Critic and auditor. Bug and issue hunting.",
193
+ preferred_models=["deepseek-v4-pro", "kimi-k2.6", "mistral-large-3", "gpt-oss-120b"],
194
+ complexity="medium",
195
+ tags=["audit", "security", "review", "critic"]
196
+ ),
197
+ }
198
+ if os.path.exists(ROLES_FILE):
199
+ try:
200
+ with open(ROLES_FILE, "r", encoding="utf-8") as f:
201
+ custom = json.load(f)
202
+ for k, v in custom.items():
203
+ if k not in defaults:
204
+ defaults[k] = Role(**v)
205
+ except Exception as e:
206
+ print(f"โš ๏ธ ะžัˆะธะฑะบะฐ ะทะฐะณั€ัƒะทะบะธ roles.json: {e}")
207
+ self.roles = defaults
208
+
209
+ def _load_conductors(self):
210
+ defaults = {
211
+ "default": Conductor(
212
+ name="default",
213
+ prompt="""You are Conductor PinkSky (Default). Analyze request and choose optimal roles and models.
214
+
215
+ RULES:
216
+ 1. Simple questions -- 1 role, 1 model.
217
+ 2. Complex tasks -- decompose, assign roles.
218
+ 3. Consider cost: cheap for simple, powerful for complex.
219
+ 4. If code -- add critic.
220
+ 5. If architecture -- add architect.
221
+
222
+ AVAILABLE ROLES: guru, hacker, architect, principal, evangelist, techlead, qa, sdet, qe, researcher, critic, universal.
223
+
224
+ AVAILABLE MODELS (by coding rank, best to worst):
225
+ TIER 1 (Elite): deepseek-v4-pro, kimi-k2.6, qwen3.5-397b, mistral-large-3, gpt-oss-120b
226
+ 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
227
+ TIER 3 (Good): step-3.7-flash, mistral-small-4, minimax-m2.7, nemotron-super-49b-v1, llama-3.2-90b-vision
228
+ TIER 4 (Fast): nemotron-nano-12b, nemotron-3-nano-30b, nemotron-nano-9b, nemotron-content-safety
229
+ TIER 5 (Specialized): nemotron-3-nano-omni, diffusiongemma
230
+
231
+ FORMAT (STRICT JSON):
232
+ {"strategy": "single|sequential|parallel", "tasks": [{"role": "role_name", "model": "model_name", "prompt": "subtask"}], "synthesis_prompt": "how to combine"}""",
233
+ description="Standard conductor -- balance of quality and speed",
234
+ strategy="selective",
235
+ max_agents=3,
236
+ cost_aware=True,
237
+ auto_rank_by="balanced"
238
+ ),
239
+ "strict": Conductor(
240
+ name="strict",
241
+ prompt="""You are Strict Conductor PinkSky. Minimum agents, maximum efficiency.
242
+
243
+ RULES:
244
+ 1. ONLY one role and one model.
245
+ 2. Cheapest model capable of solving the task.
246
+ 3. Only sequential.
247
+
248
+ FORMAT (STRICT JSON):
249
+ {"strategy": "single", "tasks": [{"role": "name", "model": "name", "prompt": "task"}], "synthesis_prompt": ""}""",
250
+ description="Minimum agents, minimum cost",
251
+ strategy="single",
252
+ max_agents=1,
253
+ cost_aware=True,
254
+ auto_rank_by="coding"
255
+ ),
256
+ "creative": Conductor(
257
+ name="creative",
258
+ prompt="""You are Creative Conductor PinkSky. Maximum perspectives, brainstorm.
259
+
260
+ RULES:
261
+ 1. Multiple roles from different angles.
262
+ 2. Parallel strategy.
263
+ 3. guru + hacker + researcher + critic.
264
+ 4. Do not save on models -- use the best.
265
+
266
+ FORMAT (STRICT JSON):
267
+ {"strategy": "parallel", "tasks": [...], "synthesis_prompt": "synthesize creative ideas"}""",
268
+ description="Maximum roles, creative brainstorm",
269
+ strategy="parallel",
270
+ max_agents=5,
271
+ cost_aware=False,
272
+ auto_rank_by="coding"
273
+ ),
274
+ "economy": Conductor(
275
+ name="economy",
276
+ prompt="""You are Economy Conductor PinkSky. Solve task for minimum cost.
277
+
278
+ RULES:
279
+ 1. Start with TIER 4 (fast/cheap): nemotron-nano-9b, nemotron-nano-12b, nemotron-3-nano-30b.
280
+ 2. Only if it fails -- escalate to TIER 3/2.
281
+ 3. One role, one model.
282
+
283
+ FORMAT (STRICT JSON):
284
+ {"strategy": "single", "tasks": [{"role": "name", "model": "name", "prompt": "task"}], "synthesis_prompt": ""}""",
285
+ description="Cheap models, budget saving",
286
+ strategy="single",
287
+ max_agents=1,
288
+ cost_aware=True,
289
+ auto_rank_by="speed"
290
+ ),
291
+ "review": Conductor(
292
+ name="review",
293
+ prompt="""You are Code Review Conductor PinkSky. Maximum quality code review.
294
+
295
+ RULES:
296
+ 1. techlead (architectural review) + critic (bugs/vulnerabilities) + guru (best practices).
297
+ 2. Parallel review.
298
+ 3. Synthesize into structured report.
299
+
300
+ FORMAT (STRICT JSON):
301
+ {"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"}""",
302
+ description="Focus on code review. Multi-angle code check.",
303
+ strategy="parallel",
304
+ max_agents=4,
305
+ cost_aware=True,
306
+ auto_rank_by="coding"
307
+ ),
308
+ "build": Conductor(
309
+ name="build",
310
+ prompt="""You are Project Build Conductor PinkSky. Build full project from spec.
311
+
312
+ RULES:
313
+ 1. Sequential: architect -> guru/hacker -> sdet -> critic.
314
+ 2. Each stage -- separate call.
315
+
316
+ FORMAT (STRICT JSON):
317
+ {"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"}""",
318
+ description="Project build. Architecture -> code -> tests -> audit.",
319
+ strategy="sequential",
320
+ max_agents=5,
321
+ cost_aware=True,
322
+ auto_rank_by="coding"
323
+ ),
324
+ }
325
+ if os.path.exists(CONDUCTORS_FILE):
326
+ try:
327
+ with open(CONDUCTORS_FILE, "r", encoding="utf-8") as f:
328
+ custom = json.load(f)
329
+ for k, v in custom.items():
330
+ if k not in defaults:
331
+ defaults[k] = Conductor(**v)
332
+ except Exception as e:
333
+ print(f"โš ๏ธ ะžัˆะธะฑะบะฐ ะทะฐะณั€ัƒะทะบะธ conductors.json: {e}")
334
+ self.conductors = defaults
335
+
336
+ def _load_history(self):
337
+ if os.path.exists(HISTORY_FILE):
338
+ try:
339
+ with open(HISTORY_FILE, "r", encoding="utf-8") as f:
340
+ data = json.load(f)
341
+ self.chat_history = data.get("chat", [])
342
+ self.skill_history = data.get("skill", [])
343
+ self.build_history = data.get("build", [])
344
+ except Exception as e:
345
+ print(f"โš ๏ธ ะžัˆะธะฑะบะฐ ะทะฐะณั€ัƒะทะบะธ ะธัั‚ะพั€ะธะธ: {e}")
346
+
347
+ def save_roles(self):
348
+ data = {k: {"name": v.name, "prompt": v.prompt, "description": v.description,
349
+ "preferred_models": v.preferred_models, "complexity": v.complexity, "tags": v.tags}
350
+ for k, v in self.roles.items()}
351
+ with open(ROLES_FILE, "w", encoding="utf-8") as f:
352
+ json.dump(data, f, ensure_ascii=False, indent=2)
353
+
354
+ def save_models(self):
355
+ data = {k: {"name": v.name, "provider": v.provider, "endpoint": v.endpoint,
356
+ "api_key_env": v.api_key_env, "context_window": v.context_window,
357
+ "max_tokens": v.max_tokens, "cost_per_1k_input": v.cost_per_1k_input,
358
+ "cost_per_1k_output": v.cost_per_1k_output,
359
+ "coding_rank": v.coding_rank, "speed_rank": v.speed_rank, "reasoning_rank": v.reasoning_rank,
360
+ "tags": v.tags}
361
+ for k, v in self.models.items()}
362
+ with open(MODELS_FILE, "w", encoding="utf-8") as f:
363
+ json.dump(data, f, ensure_ascii=False, indent=2)
364
+
365
+ def save_conductors(self):
366
+ data = {k: {"name": v.name, "prompt": v.prompt, "description": v.description,
367
+ "strategy": v.strategy, "max_agents": v.max_agents, "cost_aware": v.cost_aware,
368
+ "auto_rank_by": v.auto_rank_by}
369
+ for k, v in self.conductors.items()}
370
+ with open(CONDUCTORS_FILE, "w", encoding="utf-8") as f:
371
+ json.dump(data, f, ensure_ascii=False, indent=2)
372
+
373
+ def save_history(self):
374
+ data = {"chat": self.chat_history, "skill": self.skill_history, "build": self.build_history}
375
+ with open(HISTORY_FILE, "w", encoding="utf-8") as f:
376
+ json.dump(data, f, ensure_ascii=False, indent=2)
377
+
378
+ def add_to_history(self, mode: str, role: str, content: str):
379
+ entry = {"role": role, "content": content, "timestamp": datetime.now().isoformat()}
380
+ if mode == "chat":
381
+ self.chat_history.append(entry)
382
+ elif mode == "skill":
383
+ self.skill_history.append(entry)
384
+ elif mode == "build":
385
+ self.build_history.append(entry)
386
+ self.save_history()
387
+
388
+ def get_best_model(self, rank_by: str = "coding", min_tier: int = 1, max_tier: int = 5, exclude: List[str] = None) -> str:
389
+ exclude = exclude or []
390
+ candidates = []
391
+ for name, model in self.models.items():
392
+ if name in exclude or name == "hf_fallback":
393
+ continue
394
+ tier = 5
395
+ if model.coding_rank <= 5: tier = 1
396
+ elif model.coding_rank <= 12: tier = 2
397
+ elif model.coding_rank <= 18: tier = 3
398
+ elif model.coding_rank <= 24: tier = 4
399
+ if min_tier <= tier <= max_tier:
400
+ candidates.append((name, model))
401
+ if not candidates:
402
+ return "deepseek-v4-pro"
403
+ if rank_by == "coding":
404
+ candidates.sort(key=lambda x: x[1].coding_rank)
405
+ elif rank_by == "speed":
406
+ candidates.sort(key=lambda x: x[1].speed_rank)
407
+ elif rank_by == "reasoning":
408
+ candidates.sort(key=lambda x: x[1].reasoning_rank)
409
+ elif rank_by == "balanced":
410
+ candidates.sort(key=lambda x: (x[1].coding_rank + x[1].speed_rank + x[1].reasoning_rank) / 3)
411
+ else:
412
+ candidates.sort(key=lambda x: x[1].coding_rank)
413
+ return candidates[0][0]
414
+
415
+ def get_model_for_role(self, role_name: str, preference: str = None, rank_by: str = None) -> str:
416
+ role = self.roles.get(role_name)
417
+ if not role:
418
+ return preference or self.current_model
419
+ conductor = self.conductors.get(self.current_conductor, self.conductors["default"])
420
+ rank_criteria = rank_by or conductor.auto_rank_by
421
+ max_tier = 5
422
+ if role.complexity == "high":
423
+ max_tier = 2
424
+ elif role.complexity == "medium":
425
+ max_tier = 3
426
+ if preference and preference in self.models:
427
+ return preference
428
+ available = [m for m in role.preferred_models if m in self.models and m != "hf_fallback"]
429
+ if available:
430
+ if conductor.cost_aware and rank_criteria != "coding":
431
+ available.sort(key=lambda m: self.models[m].cost_per_1k_output)
432
+ else:
433
+ if rank_criteria == "coding":
434
+ available.sort(key=lambda m: self.models[m].coding_rank)
435
+ elif rank_criteria == "speed":
436
+ available.sort(key=lambda m: self.models[m].speed_rank)
437
+ elif rank_criteria == "reasoning":
438
+ available.sort(key=lambda m: self.models[m].reasoning_rank)
439
+ else:
440
+ available.sort(key=lambda m: (self.models[m].coding_rank + self.models[m].speed_rank + self.models[m].reasoning_rank) / 3)
441
+ return available[0]
442
+ return self.get_best_model(rank_by=rank_criteria, max_tier=max_tier)
443
+
444
+ def get_models_by_tier(self, tier: int) -> List[str]:
445
+ result = []
446
+ for name, model in self.models.items():
447
+ if name == "hf_fallback":
448
+ continue
449
+ model_tier = 5
450
+ if model.coding_rank <= 5: model_tier = 1
451
+ elif model.coding_rank <= 12: model_tier = 2
452
+ elif model.coding_rank <= 18: model_tier = 3
453
+ elif model.coding_rank <= 24: model_tier = 4
454
+ if model_tier == tier:
455
+ result.append(name)
456
+ return result
457
+
458
+ def get_next_tier_model(self, current_model_name: str) -> Optional[str]:
459
+ if current_model_name not in self.models:
460
+ return None
461
+ current = self.models[current_model_name]
462
+ current_tier = 5
463
+ if current.coding_rank <= 5: current_tier = 1
464
+ elif current.coding_rank <= 12: current_tier = 2
465
+ elif current.coding_rank <= 18: current_tier = 3
466
+ elif current.coding_rank <= 24: current_tier = 4
467
+ next_tier = current_tier + 1
468
+ if next_tier > 5:
469
+ return None
470
+ models_in_tier = self.get_models_by_tier(next_tier)
471
+ if models_in_tier:
472
+ return models_in_tier[0]
473
+ return None
474
+
475
+ def export_history_json(self) -> str:
476
+ 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)
477
+
478
+ def export_history_md(self) -> str:
479
+ lines = ["# PinkSky History Export", ""]
480
+ lines.append("*Exported: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "*")
481
+ lines.append("")
482
+ for mode, history in [("Chat", self.chat_history), ("Skill", self.skill_history), ("Build", self.build_history)]:
483
+ lines.append("## " + mode + " Mode")
484
+ lines.append("")
485
+ for entry in history:
486
+ ts = entry.get("timestamp", "unknown")
487
+ role = entry.get("role", "unknown")
488
+ content = entry.get("content", "")
489
+ lines.append("### " + role + " (" + ts + ")")
490
+ lines.append("")
491
+ lines.append("```")
492
+ lines.append(content[:500])
493
+ lines.append("```")
494
+ lines.append("")
495
+ return "\n".join(lines)
496
+
497
+ STATE = PinkSkyState()
telegram_utils.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Telegram ัƒั‚ะธะปะธั‚ั‹: ะพั‚ะฟั€ะฐะฒะบะฐ ัะพะพะฑั‰ะตะฝะธะน ะธ ั„ะฐะนะปะพะฒ"""
2
+ import os
3
+ import json
4
+ import urllib.request
5
+ import time
6
+ import logging
7
+ import certifi
8
+ import requests
9
+ from requests.adapters import HTTPAdapter
10
+ from urllib3.util.retry import Retry
11
+ from .config import CF_URL, TOKEN
12
+
13
+ class TelegramUtils:
14
+ def __init__(self):
15
+ self.logger = logging.getLogger(__name__)
16
+ self.session = requests.Session()
17
+ retry = Retry(
18
+ total=3,
19
+ backoff_factor=1,
20
+ status_forcelist=[500, 502, 503, 504]
21
+ )
22
+ self.session.mount("https://", HTTPAdapter(max_retries=retry))
23
+
24
+ def send_tg(self, chat_id, text):
25
+ if not CF_URL or not TOKEN:
26
+ self.logger.warning("TG send aborted: CF_URL or TOKEN not set")
27
+ return
28
+ safe_text = str(text)[-4000:]
29
+ url = f"{CF_URL}/bot{TOKEN}/sendMessage"
30
+ data = json.dumps({
31
+ "chat_id": chat_id,
32
+ "text": safe_text,
33
+ "parse_mode": "Markdown"
34
+ }).encode('utf-8')
35
+ headers = {
36
+ "Content-Type": "application/json",
37
+ "User-Agent": "PinkSky/7.0 (Linux; Python)"
38
+ }
39
+ try:
40
+ response = self.session.post(url, data=data, headers=headers, timeout=15, verify=certifi.where())
41
+ response.raise_for_status()
42
+ except Exception as e:
43
+ self.logger.error(f"TG send error: {e}")
44
+
45
+ def download_tg_file(self, file_id, file_name, retries=3):
46
+ if not TOKEN:
47
+ self.logger.warning("TG download aborted: TOKEN not set")
48
+ return None
49
+ base_api = CF_URL if CF_URL else "https://api.telegram.org"
50
+ get_file_url = f"{base_api}/bot{TOKEN}/getFile?file_id={file_id}"
51
+ for attempt in range(retries):
52
+ try:
53
+ response = self.session.get(get_file_url, timeout=15, verify=certifi.where())
54
+ response.raise_for_status()
55
+ res = response.json()
56
+ if not res.get("ok"):
57
+ return None
58
+ file_path = res["result"]["file_path"]
59
+ dl_url = f"{base_api}/file/bot{TOKEN}/{file_path}"
60
+ os.makedirs("downloads", exist_ok=True)
61
+ safe_name = os.path.basename(file_name)
62
+ local_path = os.path.join("downloads", safe_name)
63
+
64
+ response = self.session.get(dl_url, stream=True, timeout=60, verify=certifi.where())
65
+ response.raise_for_status()
66
+ with open(local_path, "wb") as f:
67
+ for chunk in response.iter_content(chunk_size=8192):
68
+ if chunk:
69
+ f.write(chunk)
70
+ return local_path
71
+ except Exception as e:
72
+ self.logger.warning(f"TG download attempt {attempt + 1} failed: {e}")
73
+ time.sleep(2)
74
+ return None
75
+
76
+ def send_tg_file(self, chat_id, file_path):
77
+ if not os.path.exists(file_path):
78
+ self.send_tg(chat_id, f"โ–ธ โœ— แด‡ส€ส€แดส€\nFile not found: `{file_path}`")
79
+ return
80
+ base_api = CF_URL if CF_URL else "https://api.telegram.org"
81
+ url = f"{base_api}/bot{TOKEN}/sendDocument"
82
+ self.send_tg(chat_id, f"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n โ”‚ ๐Ÿ“ค ๊œฐษชสŸแด‡ แดœแด˜สŸแดแด€แด…ษชษดษข โ”‚\n โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n โ”‚ `{os.path.basename(file_path)}` โ”‚\nโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ")
83
+ try:
84
+ with open(file_path, 'rb') as f:
85
+ files = {'document': f}
86
+ response = self.session.post(url, data={'chat_id': chat_id}, files=files, timeout=60, verify=certifi.where())
87
+ response.raise_for_status()
88
+ except Exception as e:
89
+ self.send_tg(chat_id, f"โ–ธ โš ๏ธ ๊œฐษชสŸแด‡ ษชssแดœแด‡\n{str(e)[:200]}")
90
+
91
+ # Singleton instance
92
+ TG_UTILS = TelegramUtils()
93
+ send_tg = TG_UTILS.send_tg
94
+ download_tg_file = TG_UTILS.download_tg_file
95
+ send_tg_file = TG_UTILS.send_tg_file
96
+
universal_agent.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ะฃะฝะธะฒะตั€ัะฐะปัŒะฝั‹ะน ะฐะณะตะฝั‚ ั ะฟะพะดะดะตั€ะถะบะพะน Open Interpreter"""
2
+ import json
3
+ import logging
4
+ import urllib.request
5
+ import re
6
+ from datetime import datetime
7
+ from typing import Dict, List, Optional, Any
8
+ from .models import Role, ModelConfig
9
+ from .config import API_KEY, API_BASE, HF_TOKEN
10
+ from .file_manager import FILE_MANAGER
11
+ from .internet_agent import INTERNET_AGENT
12
+ from .notification_system import NOTIFICATIONS
13
+ from .process_manager import PROCESS_MANAGER
14
+ from .state import STATE
15
+ from .interpreter_config import configure_interpreter_for_model
16
+
17
+ class UniversalAgent:
18
+ def __init__(self, role: Role, model: ModelConfig, use_interpreter: bool = False):
19
+ self.role = role
20
+ self.model = model
21
+ self.use_interpreter = use_interpreter
22
+ self.conversation_history: List[Dict[str, str]] = []
23
+ self.file_manager = FILE_MANAGER
24
+ self.internet = INTERNET_AGENT
25
+ self.notifications = NOTIFICATIONS
26
+ self.logger = logging.getLogger(__name__)
27
+ self.tools = {
28
+ "search_web": self.internet.search_web,
29
+ "fetch_page": self.internet.fetch_page,
30
+ "analyze_website": self.internet.analyze_website,
31
+ "read_file": self.file_manager.read_file,
32
+ "save_file": self.file_manager.save_file,
33
+ "list_files": self.file_manager.list_files,
34
+ "analyze_file": self.file_manager.analyze_file,
35
+ }
36
+
37
+ def _add_tools_to_task(self, task: str) -> str:
38
+ tools_desc = "\n\n๐Ÿ”ง แด€แด แด€ษชสŸแด€ส™สŸแด‡ แด›แดแดสŸs๏ผˆ๏ฝกโ—•โ€ฟโ—•๏ฝก๏ผ‰:\n"
39
+ for name, func in self.tools.items():
40
+ tools_desc += f"โ–น {name}: {func.__doc__ or 'No description'}\n"
41
+ return task + tools_desc
42
+
43
+ def execute(self, task: str, sys_prompt_override: str = None, chat_id: str = None,
44
+ history: List[Dict[str, str]] = None, mode: str = "chat") -> str:
45
+ start_time = datetime.now()
46
+ self.logger.info(f"Executing task in {mode} mode with role {self.role.name} and model {self.model.name}")
47
+
48
+ if self.use_interpreter and mode in ("skill", "build"):
49
+ result = self._execute_with_interpreter(task, chat_id, mode)
50
+ else:
51
+ result = self._execute_with_api(task, sys_prompt_override, chat_id, history, mode)
52
+
53
+ duration = (datetime.now() - start_time).total_seconds()
54
+ self.logger.info(f"Task completed in {duration:.2f}s. Tokens: {len(result)} chars")
55
+ return result
56
+
57
+ def _execute_with_interpreter(self, task: str, chat_id: str = None, mode: str = "chat") -> str:
58
+ if not configure_interpreter_for_model(self.model.name, STATE, API_KEY, API_BASE):
59
+ return "โ–ธ โš ๏ธ ษชษดแด›แด‡ส€แด˜ส€แด‡แด›แด‡ส€ แด…ษชsแด€ส™สŸแด‡แด…\nFalling back to API..."
60
+ try:
61
+ from interpreter import interpreter
62
+ base_instructions = interpreter.custom_instructions or ""
63
+ interpreter.custom_instructions = (
64
+ f"{base_instructions}\n\n"
65
+ f"๐ŸŽญ แด„แดœส€ส€แด‡ษดแด› ส€แดสŸแด‡\n{self.role.prompt[:1000]}"
66
+ )
67
+ messages = interpreter.chat(task, display=False)
68
+ interpreter.custom_instructions = base_instructions
69
+ if messages and len(messages) > 0:
70
+ return f"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n โ”‚ โœ… แด›แด€sแด‹ แด„แดแดแด˜สŸแด‡แด›แด‡แด… โ”‚\n โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n{str(messages[-1].get('content', ''))[-2000:]}\nโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ"
71
+ return "โ–ธ โš ๏ธ ษชษดแด›แด‡ส€แด˜ส€แด‡แด›แด‡ส€: No response."
72
+ except Exception as e:
73
+ self.logger.error(f"Interpreter error: {e}")
74
+ return f"โ–ธ โš ๏ธ ษชษดแด›แด‡ส€แด˜ส€แด‡แด›แด‡ส€ แด‡ส€ส€แดส€\n{str(e)[:500]}\nFalling back to API..."
75
+
76
+ def _execute_with_api(self, task: str, sys_prompt_override: str = None, chat_id: str = None,
77
+ history: List[Dict[str, str]] = None, mode: str = "chat") -> str:
78
+ system_prompt = sys_prompt_override or self.role.prompt
79
+ messages = [{"role": "system", "content": system_prompt}]
80
+ if history:
81
+ for h in history[-10:]:
82
+ messages.append({"role": h.get("role", "user"), "content": h.get("content", "")})
83
+ messages.append({"role": "user", "content": task})
84
+
85
+ if self.model.provider == "hf":
86
+ return self._call_hf(messages, chat_id)
87
+ return self._call_openai_compatible(messages, chat_id)
88
+
89
+ def _call_openai_compatible(self, messages: List[Dict], chat_id: str = None) -> str:
90
+ url = f"{API_BASE}/chat/completions"
91
+ data = json.dumps({
92
+ "model": self.model.endpoint,
93
+ "messages": messages,
94
+ "temperature": 0.7,
95
+ "max_tokens": self.model.max_tokens
96
+ }).encode('utf-8')
97
+ headers = {
98
+ "Authorization": f"Bearer {API_KEY}",
99
+ "Content-Type": "application/json"
100
+ }
101
+ req = urllib.request.Request(url, data=data, headers=headers)
102
+ try:
103
+ with urllib.request.urlopen(req, timeout=45) as response:
104
+ res = json.loads(response.read().decode('utf-8'))
105
+ content = res['choices'][0]['message']['content']
106
+ return (
107
+ "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n"
108
+ " โ”‚ ๐Ÿค– แด€ษขแด‡ษดแด› ส€แด‡sแด˜แดษดsแด‡ โ”‚\n"
109
+ "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n"
110
+ f"{content[:2000]}\n"
111
+ "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ"
112
+ )
113
+ except Exception as e:
114
+ self.logger.error(f"Primary API failed: {e}")
115
+ return self._tiered_fallback(messages, e, chat_id)
116
+
117
+ def _tiered_fallback(self, messages: List[Dict], primary_error, chat_id: str = None) -> str:
118
+ if chat_id:
119
+ from .telegram_utils import send_tg
120
+ send_tg(chat_id, "โ–ธ โš ๏ธ แดแดแด…แด‡สŸ แด‡ส€ส€แดส€\nFallback to cheaper tier...")
121
+ tried = [self.model.name]
122
+ current = self.model.name
123
+ while True:
124
+ next_model = STATE.get_next_tier_model(current)
125
+ if not next_model or next_model in tried:
126
+ break
127
+ tried.append(next_model)
128
+ model_cfg = STATE.models.get(next_model)
129
+ if not model_cfg:
130
+ break
131
+ try:
132
+ agent = UniversalAgent(self.role, model_cfg)
133
+ if model_cfg.provider == "hf":
134
+ result = agent._call_hf(messages, chat_id)
135
+ else:
136
+ result = agent._call_openai_compatible(messages, chat_id)
137
+ return (
138
+ f"{result}\n\n"
139
+ "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n"
140
+ " โ”‚ โœ… แด›ษชแด‡ส€แด‡แด… ๊œฐแด€สŸสŸส™แด€แด„แด‹ โ”‚\n"
141
+ f" โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n"
142
+ f" โ”‚ {next_model} โ”‚\n"
143
+ "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ"
144
+ )
145
+ except Exception as e:
146
+ current = next_model
147
+ return self._fallback_hf(messages, primary_error, chat_id, tried)
148
+