Web3CyberAgent / api /index.py
web3cyberservices
Update index.py
919f556 unverified
Raw
History Blame Contribute Delete
27.3 kB
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
# Безопасная инициализация лока внутри запущенного event loop
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("/")
# --- АСИНХРОННАЯ СЕТЕВАЯ УСТОЙЧИВОСТЬ TG ---
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
# Если падает из-за разметки Markdown, убираем форматирование и шлем чистый текст
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)
# --- АСИНХРОННЫЙ РАНТАЙМ-ВАЛИДАТОР (SMOKE-ТЕСТ) ---
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 = ""
# Безопасное использование лока через app.state
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)
# Перехватчик: если плагин вернул корутину (забыл await внутри), дожидаемся её
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 = "❌ Критический сбой контура обработки команд."
# Финальная защита от сбоев JSON-сериализатора (Гарантия строки)
if resp is not None and not isinstance(resp, str):
resp = str(resp)
return resp
# --- ВЕБХУК ДИСПЕТЧЕРА ДЛЯ TELEGRAM ---
@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"}
# --- ВЕБХУК ДЛЯ КЛИЕНТСКИХ БРАУЗЕРНЫХ РАСШИРЕНИЙ CHROME ---
@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"}