Spaces:
Runtime error
Runtime error
Upload 9 files
Browse files- build_mode_editor.py +86 -0
- file_manager.py +87 -0
- internet_agent.py +130 -0
- main.py +72 -0
- mode_handlers.py +123 -0
- process_manager.py +85 -0
- state.py +497 -0
- telegram_utils.py +96 -0
- 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 |
+
|