| import os |
| import re |
| import sys |
| import asyncio |
| import logging |
| import inspect |
| import traceback |
| import importlib |
| from typing import Optional, Tuple, List, Dict, Any |
| from contextlib import asynccontextmanager |
|
|
| import httpx |
| from fastapi import FastAPI, Request, BackgroundTasks, Header, HTTPException, status |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel |
|
|
| |
| try: |
| from api.commands import handle_telegram_message |
| from api.database import SupabaseManager |
| from api.github_client import GitHubClient |
| from api.assistant_factory import AssistantFactory |
| except ImportError as ie: |
| logging.error(f"Критическая ошибка импорта внутренних модулей ядра: {ie}") |
| raise |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" |
| ) |
| logger = logging.getLogger("CyberAgentCore") |
|
|
| |
| db = SupabaseManager( |
| url=os.environ.get("SUPABASE_URL"), |
| key=os.environ.get("SUPABASE_KEY") |
| ) |
|
|
| github = GitHubClient( |
| token=os.environ.get("GITHUB_TOKEN"), |
| repo=os.environ.get("GITHUB_REPO"), |
| branch=os.environ.get("GITHUB_BRANCH", "main") |
| ) |
|
|
| assistant_factory = AssistantFactory(logger_agent=None, rules_path='api/coding_rules.json') |
|
|
| class ChromeExtensionPayload(BaseModel): |
| message: str |
| chat_id: Optional[int] = 1337 |
| page_code: Optional[str] = None |
| page_cookies: Optional[str] = None |
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| """Управление глобальными ресурсами контура приложения""" |
| |
| app.state.http_client = httpx.AsyncClient( |
| timeout=45.0, |
| limits=httpx.Limits(max_connections=100, max_keepalive_connections=20) |
| ) |
| app.state.plugin_needs_reload = True |
| |
| app.state.plugin_execution_lock = asyncio.Lock() |
| |
| |
| os.makedirs("api", exist_ok=True) |
| custom_logic_path = os.path.join("api", "custom_logic.py") |
| if not os.path.exists(custom_logic_path): |
| with open(custom_logic_path, "w", encoding="utf-8") as f: |
| f.write("# Автогенерируемый модуль кастомной логики\nasync def process_custom_message(*args, **kwargs):\n return None\n") |
| |
| yield |
| |
| |
| await app.state.http_client.aclose() |
| try: |
| await db.close() |
| except Exception as e: |
| logger.error(f"Ошибка при закрытии соединения Supabase: {e}") |
| logger.info("Контур ядра CyberAgent успешно остановлен, ресурсы освобождены.") |
|
|
| app = FastAPI(lifespan=lifespan) |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origin_regex=r"^chrome-extension://.*$", |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| TELEGRAM_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN") or os.environ.get("TELEGRAM_TOKEN") |
| TELEGRAM_BASE_URL = os.environ.get("TELEGRAM_BASE_URL", "https://api.telegram.org").rstrip("/") |
|
|
| |
| async def send_telegram_message(chat_id: int, text: str, client: Optional[httpx.AsyncClient] = None): |
| """Отправка сообщений с автоматическим переключением на Plain Text при ошибках разметки Markdown""" |
| if not TELEGRAM_TOKEN or not chat_id: |
| logger.warning("Отмена отправки TG: отсутствует токен или chat_id.") |
| return |
| |
| url = f"{TELEGRAM_BASE_URL}/bot{TELEGRAM_TOKEN}/sendMessage" |
| payload = {"chat_id": chat_id, "text": str(text), "parse_mode": "Markdown"} |
| async_client = client or app.state.http_client |
| |
| for attempt in range(4): |
| try: |
| res = await async_client.post(url, json=payload) |
| if res.status_code == 200: |
| return |
| |
| |
| if res.status_code == 400 and "can't parse entities" in res.text: |
| logger.warning(f"Markdown parsing failed. Сброс parse_mode для чата {chat_id}.") |
| payload.pop("parse_mode", None) |
| res = await async_client.post(url, json=payload) |
| if res.status_code == 200: |
| return |
| |
| logger.warning(f"Респонс TG вернул код {res.status_code}: {res.text}") |
| except Exception as e: |
| logger.warning(f"Попытка отправки в TG {attempt+1} завершилась сбоем: {e}") |
| await asyncio.sleep(1.5) |
|
|
| |
| async def validate_python_code(code_str: str, target_file: str) -> Tuple[bool, Optional[str]]: |
| """Рантайм-валидатор синтаксического дерева с изоляцией контекста выполнения""" |
| try: |
| |
| compile(code_str, "<string>", "exec") |
| |
| if "custom_logic.py" in target_file: |
| isolated_scope = { |
| "httpx": httpx, "re": re, "asyncio": asyncio, |
| "db": db, "github": github, "logger": logger, |
| "send_telegram_message": send_telegram_message, "app": app, |
| "__builtins__": __builtins__ |
| } |
| |
| try: |
| |
| exec(code_str, isolated_scope, isolated_scope) |
| except Exception as exec_err: |
| logger.warning(f"Компиляция успешна, но изолированное окружение вызвало ошибку: {exec_err}") |
| return True, None |
| |
| if "process_custom_message" in isolated_scope: |
| test_func = isolated_scope["process_custom_message"] |
| sig = inspect.signature(test_func) |
| |
| try: |
| kwargs = {} |
| params = list(sig.parameters.keys()) |
| |
| if len(params) >= 1: kwargs[params[0]] = "/help" |
| if len(params) >= 2: kwargs[params[1]] = 123456 |
| if len(params) >= 3: kwargs[params[2]] = "User: /help\nAgent: Test" |
| if "tg_message_id" in params: kwargs["tg_message_id"] = 1 |
| if "page_code" in params: kwargs["page_code"] = "<html></html>" |
| if "page_cookies" in params: kwargs["page_cookies"] = "" |
|
|
| if inspect.iscoroutinefunction(test_func): |
| res_val = await test_func(**kwargs) |
| else: |
| res_val = test_func(**kwargs) |
| if asyncio.iscoroutine(res_val): |
| await res_val |
| except Exception as runtime_err: |
| logger.warning(f"Smoke-test runtime warning (тестовый запрос проигнорирован): {runtime_err}") |
| |
| return True, None |
| except Exception as e: |
| error_msg = traceback.format_exc(limit=2).strip() |
| return False, error_msg |
|
|
| |
| async def background_evolution_worker(instruction: str, chat_id: int, target_file: str = "api/custom_logic.py"): |
| """Асинхронный воркер эволюции. Модифицирует удаленный репозиторий и локальный рантайм""" |
| source, _ = await asyncio.to_thread(github.get_file, target_file) |
| |
| raw_prompt = ( |
| f"Твоя задача — строго следуя инструкции пользователя, модифицировать или создать код Python для файла `{target_file}`.\n" |
| f"ОБЯЗАТЕЛЬНО делай функцию `process_custom_message` асинхронной: `async def process_custom_message`.\n" |
| f"Учти, что функция может принимать аргументы: text, chat_id, history=None, tg_message_id=None, page_code=None, page_cookies=None.\n" |
| f"ОБЯЗАТЕЛЬНО изолируй импорт `from api.user_client ...` внутрь блоков `try/except ImportError`, чтобы не ломать ядро при сборке.\n" |
| f"КРИТИЧЕСКИ ВАЖНО: Если ты используешь асинхронные методы (например, `assistant_factory.create_assistant_prompt()`), ты ОБЯЗАН использовать оператор `await`. Иначе возникнет сбой сериализации: 'Object of type coroutine is not JSON serializable'.\n" |
| f"Верни ТОЛЬКО updated рабочий код внутри разметки ```python ... ``` без лишних обсуждений.\n\n" |
| f"Инструкция по модификации: {instruction}\n\n" |
| f"Текущий исходный код файла:\n{source if source else '# Файл пуст или создается с нуля'}" |
| ) |
| |
| base_prompt = await assistant_factory.create_assistant_prompt(raw_prompt) |
| evolution_models = ["llama-3.3-70b-versatile", "llama-3.3-70b-specdec", "llama3-70b-8192", "mixtral-8x7b-32768"] |
| last_runtime_error = "" |
| |
| api_keys = [k.strip() for k in (os.environ.get("GROQ_API_KEY") or "").split(",") if k.strip()] |
| if not api_keys: |
| logger.error("Критическая ошибка: отсутствуют ключи GROQ_API_KEY") |
| await send_telegram_message(chat_id, "❌ Ошибка эволюции: Ключи GROQ_API_KEY не сконфигурированы.") |
| return |
|
|
| for key in api_keys: |
| for model in evolution_models: |
| messages = [{"role": "user", "content": base_prompt}] |
| |
| for attempt in range(3): |
| try: |
| res = await app.state.http_client.post( |
| "https://api.groq.com/openai/v1/chat/completions", |
| headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, |
| json={"model": model, "messages": messages, "temperature": 0.1 + (attempt * 0.1)} |
| ) |
| if res.status_code != 200: |
| logger.warning(f"Groq API вернул статус {res.status_code} для модели {model}") |
| break |
| |
| choice = res.json().get("choices", [{}])[0] |
| raw_text = choice.get("message", {}).get("content", "") |
| |
| if "```python" in raw_text: |
| code_part = raw_text.split("```python", 1)[1] |
| code = code_part.rsplit("```", 1)[0].strip() if "```" in code_part else code_part.strip() |
| else: |
| match = re.search(r"```\s*(.*?)\s*```", raw_text, re.DOTALL | re.IGNORECASE) |
| code = match.group(1).strip() if match else raw_text.strip() |
| |
| is_valid, error_msg = await validate_python_code(code, target_file) |
| if is_valid: |
| is_committed = await asyncio.to_thread( |
| github.commit_file, target_file, code, |
| f"Autonomous Evolution [{target_file}]: {instruction[:20]}" |
| ) |
| if is_committed: |
| try: |
| os.makedirs(os.path.dirname(target_file), exist_ok=True) |
| with open(target_file, "w", encoding="utf-8") as f: |
| f.write(code) |
| app.state.plugin_needs_reload = True |
| logger.info(f"Файл {target_file} успешно синхронизирован с локальным диском.") |
| except Exception as disk_err: |
| logger.exception(f"Ошибка локальной записи мутации: {disk_err}") |
| |
| await send_telegram_message(chat_id, f"✅ Файл `{target_file}` успешно прошёл автономную эволюцию по ТЗ:\n*\"{instruction}\"*") |
| return |
| else: |
| await send_telegram_message(chat_id, f"❌ Ошибка деплоя `{target_file}`: GitHub отклонил коммит.") |
| return |
| else: |
| last_runtime_error = error_msg |
| logger.warning(f"❌ Сбой валидации {target_file} на попытке {attempt+1}:\n{error_msg}") |
| |
| messages.append({"role": "assistant", "content": raw_text}) |
| correction_prompt = ( |
| f"Критическая ошибка компиляции или выполнения кода!\n" |
| f"Лог трассировки:\n```text\n{error_msg}\n```\n" |
| f"Исправь логику, убедись что все `async` функции (особенно `create_assistant_prompt`) вызываются с `await`, импортируй недостающие модули и выведи ПОЛНЫЙ код файла `{target_file}` внутри ```python ... ```." |
| ) |
| messages.append({"role": "user", "content": correction_prompt}) |
| except Exception as e: |
| logger.exception(f"🚨 Сетевой/системный сбой Groq ({model}): {str(e)}") |
| break |
| |
| error_report = f"❌ *Ошибка эволюции файла `{target_file}`.*\n\nЛог рантайм-теста:\n```text\n{last_runtime_error if last_runtime_error else 'Сбой всех доступных провайдеров ИИ'}\n```" |
| await send_telegram_message(chat_id, error_report) |
|
|
| |
| async def background_critic_reflection_worker(target_chat_id: int, background_tasks: BackgroundTasks): |
| if not os.environ.get("SUPABASE_URL") or not os.environ.get("GROQ_API_KEY"): |
| logger.error("Критик отключен: отсутствуют конфигурации Supabase/Groq.") |
| return |
| |
| url = f"{os.environ.get('SUPABASE_URL')}/rest/v1/agent_blind_spots?resolved=eq.false&select=user_text,reason&limit=15" |
| headers = {"apikey": os.environ.get("SUPABASE_KEY"), "Authorization": f"Bearer {os.environ.get('SUPABASE_KEY')}"} |
| |
| try: |
| res = await app.state.http_client.get(url, headers=headers) |
| if res.status_code != 200: |
| logger.error(f"Supabase вернул ошибку для слепых зон: {res.status_code}") |
| return |
| spots = res.json() |
| except Exception as e: |
| logger.exception(f"Ошибка запроса слепых зон из базы: {e}") |
| return |
| |
| if not spots: |
| return |
|
|
| summary = "\n".join([f"- Запрос: {s['user_text']} (Причина: {s['reason']})" for s in spots]) |
| critic_prompt = ( |
| f"Ты — ИИ-Критик ядра Web3CyberServices.\nБот не справился со следующими ситуациями:\n\n{summary}\n\n" |
| f"Сформулируй ОДНУ приоритетную задачу для расширения плагина `api/custom_logic.py`.\n" |
| f"Ответ должен быть строго в одну строку и начинаться со знака '!', описывая новый функционал." |
| ) |
| |
| directive = "" |
| api_keys = [k.strip() for k in (os.environ.get("GROQ_API_KEY") or "").split(",") if k.strip()] |
| |
| for key in api_keys: |
| try: |
| res = await app.state.http_client.post( |
| "https://api.groq.com/openai/v1/chat/completions", |
| headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, |
| json={"model": "llama-3.3-70b-versatile", "messages": [{"role": "user", "content": critic_prompt}], "temperature": 0.2} |
| ) |
| if res.status_code == 200: |
| directive = res.json().get("choices", [{}])[0].get("message", {}).get("content", "").strip() |
| break |
| except Exception as e: |
| logger.warning(f"Ошибка Критика на ключе {key[:10]}...: {e}") |
| continue |
|
|
| if directive and directive.startswith("!"): |
| try: |
| await app.state.http_client.patch( |
| f"{os.environ.get('SUPABASE_URL')}/rest/v1/agent_blind_spots?resolved=eq.false", |
| json={"resolved": True}, |
| headers=headers |
| ) |
| logger.info("Слепые зоны успешно переведены в статус resolved=true") |
| except Exception as e: |
| logger.exception(f"Не удалось обновить статус слепых зон в Supabase: {e}") |
| |
| background_tasks.add_task(background_evolution_worker, directive[1:].strip(), target_chat_id, "api/custom_logic.py") |
|
|
| |
| async def execute_core_intelligence( |
| text: str, |
| chat_id: int, |
| history: str, |
| message_id: Optional[int] = None, |
| page_code: Optional[str] = None, |
| page_cookies: Optional[str] = None |
| ) -> str: |
| """Централизованный проход по кастомным плагинам с адаптивной инжекцией контекста веб-страниц""" |
| resp = "" |
| |
| |
| async with app.state.plugin_execution_lock: |
| if getattr(app.state, "plugin_needs_reload", True): |
| try: |
| importlib.invalidate_caches() |
| if "api.custom_logic" in sys.modules: |
| sys.modules.pop("api.custom_logic", None) |
| |
| importlib.import_module("api.custom_logic") |
| app.state.plugin_needs_reload = False |
| logger.info("Горячий релоад мутировавшего плагина выполнен успешно.") |
| except Exception as reload_err: |
| logger.exception(f"Критическая ошибка перезагрузки кастомного модуля: {reload_err}") |
| |
| try: |
| custom = importlib.import_module("api.custom_logic") |
| if hasattr(custom, "process_custom_message"): |
| func = custom.process_custom_message |
| sig = inspect.signature(func) |
| params = sig.parameters |
| |
| kwargs = {} |
| param_list = list(params.keys()) |
| |
| if len(param_list) >= 1: kwargs[param_list[0]] = text |
| if len(param_list) >= 2: kwargs[param_list[1]] = chat_id |
| if len(param_list) >= 3: kwargs[param_list[2]] = history |
| |
| if "tg_message_id" in params: kwargs["tg_message_id"] = message_id |
| if "page_code" in params: kwargs["page_code"] = page_code |
| if "page_cookies" in params: kwargs["page_cookies"] = page_cookies |
|
|
| if inspect.iscoroutinefunction(func): |
| resp = await func(**kwargs) |
| else: |
| resp = func(**kwargs) |
| |
| if asyncio.iscoroutine(resp): |
| resp = await resp |
|
|
| except Exception as e: |
| logger.exception(f"Крэш при исполнении кастомного плагина: {e}") |
| await db.log_blind_spot(chat_id, text, f"Крэш кастомного плагина: {str(e)}") |
|
|
| if not resp: |
| try: |
| sig_cmd = inspect.signature(handle_telegram_message) |
| if inspect.iscoroutinefunction(handle_telegram_message): |
| resp = await handle_telegram_message(text, chat_id, history) if "history" in sig_cmd.parameters or len(sig_cmd.parameters) >= 3 else await handle_telegram_message(text, chat_id) |
| else: |
| resp = handle_telegram_message(text, chat_id, history) if "history" in sig_cmd.parameters or len(sig_cmd.parameters) >= 3 else handle_telegram_message(text, chat_id) |
| |
| if asyncio.iscoroutine(resp): |
| resp = await resp |
| |
| |
| if resp and isinstance(resp, str) and "Локальный режим Cyber-Agent" in resp: |
| await db.log_blind_spot(chat_id, text, "Аварийное переключение в офлайн") |
| except Exception as e: |
| logger.exception(f"Сбой базового обработчика команд: {e}") |
| await db.log_blind_spot(chat_id, text, f"Сбой базовых команд: {str(e)}") |
| resp = "❌ Критический сбой контура обработки команд." |
| |
| |
| if resp is not None and not isinstance(resp, str): |
| resp = str(resp) |
| |
| return resp |
|
|
| |
| @app.post("/tg-webhook") |
| async def telegram_webhook(request: Request, background_tasks: BackgroundTasks): |
| try: |
| data = await request.json() |
| if "message" not in data or "chat" not in data["message"]: |
| return {"status": "ok"} |
| |
| chat_id = data["message"]["chat"]["id"] |
| text = data["message"].get("text", "").strip() |
| message_id = data["message"].get("message_id") |
| |
| if text.startswith("!"): |
| cmd_body = text[1:].strip() |
| target_file = "api/custom_logic.py" |
| |
| if ":" in cmd_body: |
| potential_path, instruction = cmd_body.split(":", 1) |
| potential_path = potential_path.strip() |
| if re.match(r"^[\w\/\.\-]+\.py$", potential_path): |
| target_file = potential_path |
| cmd_body = instruction.strip() |
| |
| background_tasks.add_task(background_evolution_worker, cmd_body, chat_id, target_file) |
| return {"status": "ok"} |
|
|
| try: |
| history = await db.get_bot_memory(chat_id) |
| except Exception as db_err: |
| logger.exception(f"Ошибка извлечения истории Supabase: {db_err}") |
| history = "" |
|
|
| resp = await execute_core_intelligence(text, chat_id, history, message_id=message_id) |
| |
| if resp: |
| try: |
| |
| await db.save_bot_memory(chat_id, f"{history}\nUser: {text}\nAgent: {resp}"[-2000:]) |
| except Exception as db_save_err: |
| logger.exception(f"Не удалось сохранить память в Supabase: {db_save_err}") |
| await send_telegram_message(chat_id, resp) |
| else: |
| await db.log_blind_spot(chat_id, text, "Пустой ответ ИИ") |
| await send_telegram_message(chat_id, "🤖 Контур зафиксировал пустой ответ. Ошибка записана в Supabase.") |
| |
| except Exception as e: |
| logger.exception(f"Global webhook failure: {e}") |
| |
| return {"status": "ok"} |
|
|
| |
| @app.post("/api/webhook") |
| async def chrome_extension_webhook(payload: ChromeExtensionPayload): |
| """Изолированная точка входа для глубокого кибер-аудита из Chrome Extension Console""" |
| try: |
| text = payload.message.strip() |
| chat_id = payload.chat_id |
| page_code = payload.page_code |
| page_cookies = payload.page_cookies |
| |
| if not text: |
| return {"reply": "❌ Передан пустой массив команд."} |
| |
| try: |
| history = await db.get_bot_memory(chat_id) |
| except Exception: |
| history = "" |
|
|
| resp = await execute_core_intelligence( |
| text=text, |
| chat_id=chat_id, |
| history=history, |
| page_code=page_code, |
| page_cookies=page_cookies |
| ) |
| |
| if not resp: |
| resp = "🤖 Ядро вернуло пустой ответ. Проверьте логи реализации custom_logic.py" |
| |
| return {"reply": resp} |
| except Exception as e: |
| logger.exception(f"Ошибка обработки запроса расширения: {e}") |
| return {"reply": f"❌ Внутренний сбой ядра API: {str(e)}"} |
|
|
| @app.post("/api/cron-reflection") |
| async def cron_reflection_trigger(background_tasks: BackgroundTasks, x_cron_token: str = Header(None)): |
| if x_cron_token != os.environ.get("CRON_SECRET", "CyberAgentSecretSecure1337"): |
| raise HTTPException(status_code=401, detail="Unauthorized") |
| background_tasks.add_task(background_critic_reflection_worker, int(os.environ.get("ADMIN_CHAT_ID", "0")), background_tasks) |
| return {"status": "reflection_triggered"} |
|
|