Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
49 KiB
JSON
1 line
49 KiB
JSON
{"repo": "autoalexai-sketch/consilium-ai", "files": {"app/config.py": {"7c7c330c0a8453678a27221e05bf49d756c3c164": {"t": 1785745414.3077147, "n": 3730, "h": "1307e900cadff02b", "c": "\"\"\"\napp/config.py -- Settings for CONSILIUM AI v3.0.\nReads variables from .env via python-dotenv.\n\"\"\"\n\nimport os\nfrom dotenv import load_dotenv\nfrom loguru import logger\n\nload_dotenv()\n\n\nclass Settings:\n APP_NAME: str = \"Consilium AI\"\n APP_VERSION: str = \"3.0.0\"\n DESCRIPTION: str = \"Multi-agent AI Deliberation System\"\n\n # --- Database ---\n DB_PATH: str = \"consilium.db\"\n DATABASE_URL: str = os.getenv(\"DATABASE_URL\", \"sqlite:///./consilium.db\")\n\n # --- AI providers ---\n OPENROUTER_API_KEY: str = os.getenv(\"OPENROUTER_API_KEY\", \"\")\n ANTHROPIC_API_KEY: str = os.getenv(\"ANTHROPIC_API_KEY\", \"\")\n GEMINI_API_KEY: str = os.getenv(\"GEMINI_API_KEY\", \"\")\n GROQ_API_KEY: str = os.getenv(\"GROQ_API_KEY\", \"\")\n DEEPSEEK_API_KEY: str = os.getenv(\"DEEPSEEK_API_KEY\", \"\")\n\n # --- Ollama (optional, local only) ---\n OLLAMA_URL: str = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n\n # --- SMTP mail ---\n SMTP_SERVER: str = os.getenv(\"SMTP_SERVER\", \"smtp.gmail.com\")\n SMTP_PORT: int = int(os.getenv(\"SMTP_PORT\", \"587\"))\n SMTP_USER: str = os.getenv(\"SMTP_USER\", \"\")\n SMTP_PASS: str = os.getenv(\"SMTP_PASS\", \"\")\n\n # --- Security ---\n SECRET_KEY: str = os.getenv(\"SECRET_KEY\", \"\")\n\n # --- JWT ---\n JWT_ALGORITHM: str = \"HS256\"\n JWT_EXPIRE_HOURS: int = int(os.getenv(\"JWT_EXPIRE_HOURS\", \"24\"))\n\n # --- CORS ---\n CORS_ORIGINS: list = os.getenv(\"CORS_ORIGINS\", \"*\").split(\",\")\n\n # --- Stripe (credit top-up billing) ---\n STRIPE_SECRET_KEY: str = os.getenv(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET: str = os.getenv(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PUBLISHABLE_KEY: str = os.getenv(\"STRIPE_PUBLISHABLE_KEY\", \"\")\n PUBLIC_BASE_URL: str = os.getenv(\"PUBLIC_BASE_URL\", \"http://localhost:8000\")\n\n # --- hCaptcha (bot protection on /register) ---\n # Get keys at: https://dashboard.hcaptcha.com\n # If HCAPTCHA_SECRET is empty, captcha verification is skipped (dev mode).\n # Site key is public -- safe to expose in frontend JS.\n HCAPTCHA_SECRET: str = os.getenv(\"HCAPTCHA_SECRET\", \"\")\n HCAPTCHA_SITE_KEY: str = os.getenv(\"HCAPTCHA_SITE_KEY\", \"\")\n\n\n# -- Singleton instance -------------------------------------------------------\nsettings = Settings()\n\n# -- Startup checks (warnings, NOT crashes) -----------------------------------\nif not settings.OPENROUTER_API_KEY:\n logger.warning(\n \"⚠️ OPENROUTER_API_KEY not found. \"\n \"Council mode unavailable. Add key to .env\"\n )\n\nif not settings.SECRET_KEY:\n raise ValueError(\"❌ SECRET_KEY not set in .env!\")\n\nif not settings.STRIPE_SECRET_KEY:\n logger.warning(\n \"⚠️ STRIPE_SECRET_KEY not found. \"\n \"Credit top-up (billing) unavailable until configured in .env\"\n )\n\nif not settings.HCAPTCHA_SECRET:\n logger.warning(\n \"⚠️ HCAPTCHA_SECRET not set. \"\n \"Bot protection on /register is DISABLED. \"\n \"Set HCAPTCHA_SECRET + HCAPTCHA_SITE_KEY in .env to enable.\"\n )\n\n# -- Log active providers -----------------------------------------------------\n_providers = []\nif settings.OPENROUTER_API_KEY: _providers.append(\"OpenRouter\")\nif settings.ANTHROPIC_API_KEY: _providers.append(\"Claude\")\nif settings.GEMINI_API_KEY: _providers.append(\"Gemini\")\nif settings.GROQ_API_KEY: _providers.append(\"Groq\")\nif settings.DEEPSEEK_API_KEY: _providers.append(\"DeepSeek\")\nlogger.info(f\"🔑 Active providers: {', '.join(_providers) or 'none'}\")\nlogger.info(f\"🔐 JWT: algorithm={settings.JWT_ALGORITHM} | expire={settings.JWT_EXPIRE_HOURS}h\")\nlogger.info(f\"💳 Stripe billing: {'configured' if settings.STRIPE_SECRET_KEY else 'NOT configured'}\")\nlogger.info(f\"🤖 hCaptcha: {'enabled' if settings.HCAPTCHA_SECRET else 'disabled (dev mode)'}\")\n"}, "16e84861936a3f53bb510d09484b0f1bd92cd55e": {"t": 1785752958.6543453, "n": 3730, "h": "1307e900cadff02b", "c": "\"\"\"\napp/config.py -- Settings for CONSILIUM AI v3.0.\nReads variables from .env via python-dotenv.\n\"\"\"\n\nimport os\nfrom dotenv import load_dotenv\nfrom loguru import logger\n\nload_dotenv()\n\n\nclass Settings:\n APP_NAME: str = \"Consilium AI\"\n APP_VERSION: str = \"3.0.0\"\n DESCRIPTION: str = \"Multi-agent AI Deliberation System\"\n\n # --- Database ---\n DB_PATH: str = \"consilium.db\"\n DATABASE_URL: str = os.getenv(\"DATABASE_URL\", \"sqlite:///./consilium.db\")\n\n # --- AI providers ---\n OPENROUTER_API_KEY: str = os.getenv(\"OPENROUTER_API_KEY\", \"\")\n ANTHROPIC_API_KEY: str = os.getenv(\"ANTHROPIC_API_KEY\", \"\")\n GEMINI_API_KEY: str = os.getenv(\"GEMINI_API_KEY\", \"\")\n GROQ_API_KEY: str = os.getenv(\"GROQ_API_KEY\", \"\")\n DEEPSEEK_API_KEY: str = os.getenv(\"DEEPSEEK_API_KEY\", \"\")\n\n # --- Ollama (optional, local only) ---\n OLLAMA_URL: str = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n\n # --- SMTP mail ---\n SMTP_SERVER: str = os.getenv(\"SMTP_SERVER\", \"smtp.gmail.com\")\n SMTP_PORT: int = int(os.getenv(\"SMTP_PORT\", \"587\"))\n SMTP_USER: str = os.getenv(\"SMTP_USER\", \"\")\n SMTP_PASS: str = os.getenv(\"SMTP_PASS\", \"\")\n\n # --- Security ---\n SECRET_KEY: str = os.getenv(\"SECRET_KEY\", \"\")\n\n # --- JWT ---\n JWT_ALGORITHM: str = \"HS256\"\n JWT_EXPIRE_HOURS: int = int(os.getenv(\"JWT_EXPIRE_HOURS\", \"24\"))\n\n # --- CORS ---\n CORS_ORIGINS: list = os.getenv(\"CORS_ORIGINS\", \"*\").split(\",\")\n\n # --- Stripe (credit top-up billing) ---\n STRIPE_SECRET_KEY: str = os.getenv(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET: str = os.getenv(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PUBLISHABLE_KEY: str = os.getenv(\"STRIPE_PUBLISHABLE_KEY\", \"\")\n PUBLIC_BASE_URL: str = os.getenv(\"PUBLIC_BASE_URL\", \"http://localhost:8000\")\n\n # --- hCaptcha (bot protection on /register) ---\n # Get keys at: https://dashboard.hcaptcha.com\n # If HCAPTCHA_SECRET is empty, captcha verification is skipped (dev mode).\n # Site key is public -- safe to expose in frontend JS.\n HCAPTCHA_SECRET: str = os.getenv(\"HCAPTCHA_SECRET\", \"\")\n HCAPTCHA_SITE_KEY: str = os.getenv(\"HCAPTCHA_SITE_KEY\", \"\")\n\n\n# -- Singleton instance -------------------------------------------------------\nsettings = Settings()\n\n# -- Startup checks (warnings, NOT crashes) -----------------------------------\nif not settings.OPENROUTER_API_KEY:\n logger.warning(\n \"⚠️ OPENROUTER_API_KEY not found. \"\n \"Council mode unavailable. Add key to .env\"\n )\n\nif not settings.SECRET_KEY:\n raise ValueError(\"❌ SECRET_KEY not set in .env!\")\n\nif not settings.STRIPE_SECRET_KEY:\n logger.warning(\n \"⚠️ STRIPE_SECRET_KEY not found. \"\n \"Credit top-up (billing) unavailable until configured in .env\"\n )\n\nif not settings.HCAPTCHA_SECRET:\n logger.warning(\n \"⚠️ HCAPTCHA_SECRET not set. \"\n \"Bot protection on /register is DISABLED. \"\n \"Set HCAPTCHA_SECRET + HCAPTCHA_SITE_KEY in .env to enable.\"\n )\n\n# -- Log active providers -----------------------------------------------------\n_providers = []\nif settings.OPENROUTER_API_KEY: _providers.append(\"OpenRouter\")\nif settings.ANTHROPIC_API_KEY: _providers.append(\"Claude\")\nif settings.GEMINI_API_KEY: _providers.append(\"Gemini\")\nif settings.GROQ_API_KEY: _providers.append(\"Groq\")\nif settings.DEEPSEEK_API_KEY: _providers.append(\"DeepSeek\")\nlogger.info(f\"🔑 Active providers: {', '.join(_providers) or 'none'}\")\nlogger.info(f\"🔐 JWT: algorithm={settings.JWT_ALGORITHM} | expire={settings.JWT_EXPIRE_HOURS}h\")\nlogger.info(f\"💳 Stripe billing: {'configured' if settings.STRIPE_SECRET_KEY else 'NOT configured'}\")\nlogger.info(f\"🤖 hCaptcha: {'enabled' if settings.HCAPTCHA_SECRET else 'disabled (dev mode)'}\")\n"}, "32f2cfdc2f3e0eda4adbb6ed6b2ed4b38a199903": {"t": 1785752960.0215414, "n": 3290, "h": "84524a0bdbe029fc", "c": "\"\"\"\napp/config.py -- Settings for CONSILIUM AI v3.0.\nReads variables from .env via python-dotenv.\n\"\"\"\n\nimport os\nfrom dotenv import load_dotenv\nfrom loguru import logger\n\nload_dotenv()\n\n\nclass Settings:\n APP_NAME: str = \"Consilium AI\"\n APP_VERSION: str = \"3.0.0\"\n DESCRIPTION: str = \"Multi-agent AI Deliberation System\"\n\n # --- Database ---\n DB_PATH: str = \"consilium.db\"\n DATABASE_URL: str = os.getenv(\"DATABASE_URL\", \"sqlite:///./consilium.db\")\n\n # --- AI providers ---\n OPENROUTER_API_KEY: str = os.getenv(\"OPENROUTER_API_KEY\", \"\")\n ANTHROPIC_API_KEY: str = os.getenv(\"ANTHROPIC_API_KEY\", \"\")\n GEMINI_API_KEY: str = os.getenv(\"GEMINI_API_KEY\", \"\")\n GROQ_API_KEY: str = os.getenv(\"GROQ_API_KEY\", \"\")\n\n # --- Ollama (optional, local only) ---\n OLLAMA_URL: str = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n\n # --- SMTP mail ---\n SMTP_SERVER: str = os.getenv(\"SMTP_SERVER\", \"smtp.gmail.com\")\n SMTP_PORT: int = int(os.getenv(\"SMTP_PORT\", \"587\"))\n SMTP_USER: str = os.getenv(\"SMTP_USER\", \"\")\n SMTP_PASS: str = os.getenv(\"SMTP_PASS\", \"\")\n\n # --- Security ---\n SECRET_KEY: str = os.getenv(\"SECRET_KEY\", \"\")\n\n # --- JWT ---\n JWT_ALGORITHM: str = \"HS256\"\n JWT_EXPIRE_HOURS: int = int(os.getenv(\"JWT_EXPIRE_HOURS\", \"24\"))\n\n # --- CORS ---\n CORS_ORIGINS: list = os.getenv(\"CORS_ORIGINS\", \"*\").split(\",\")\n\n # --- Stripe (credit top-up billing) ---\n # All three are blank by default -- billing endpoints check for this and\n # return 503 \"Billing not configured\" rather than crashing on import, so\n # the rest of the app keeps working even before Stripe is wired up.\n STRIPE_SECRET_KEY: str = os.getenv(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET: str = os.getenv(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PUBLISHABLE_KEY: str = os.getenv(\"STRIPE_PUBLISHABLE_KEY\", \"\")\n # Base URL used to build Stripe Checkout success_url/cancel_url redirects.\n # Must match wherever the frontend is actually served from.\n PUBLIC_BASE_URL: str = os.getenv(\"PUBLIC_BASE_URL\", \"http://localhost:8000\")\n\n\n# -- Singleton instance ──────────────────────────────────────────────────────\nsettings = Settings()\n\n# -- Startup checks (warnings, NOT crashes) ──────────────────────────────────\nif not settings.OPENROUTER_API_KEY:\n logger.warning(\n \"⚠️ OPENROUTER_API_KEY not found. \"\n \"Council mode unavailable. Add key to .env\"\n )\n\nif not settings.SECRET_KEY:\n raise ValueError(\"❌ SECRET_KEY not set in .env!\")\n\nif not settings.STRIPE_SECRET_KEY:\n logger.warning(\n \"⚠️ STRIPE_SECRET_KEY not found. \"\n \"Credit top-up (billing) unavailable until configured in .env\"\n )\n\n# -- Log active providers ────────────────────────────────────────────────────\n_providers = []\nif settings.OPENROUTER_API_KEY: _providers.append(\"OpenRouter\")\nif settings.ANTHROPIC_API_KEY: _providers.append(\"Claude\")\nif settings.GEMINI_API_KEY: _providers.append(\"Gemini\")\nif settings.GROQ_API_KEY: _providers.append(\"Groq\")\nlogger.info(f\"🔑 Active providers: {', '.join(_providers) or 'none'}\")\nlogger.info(f\"🔐 JWT: algorithm={settings.JWT_ALGORITHM} | expire={settings.JWT_EXPIRE_HOURS}h\")\nlogger.info(f\"💳 Stripe billing: {'configured' if settings.STRIPE_SECRET_KEY else 'NOT configured'}\")\n"}, "2808d4722da7f73abadedfea8c6e57aa1c44db7c": {"t": 1785752961.416122, "n": 2306, "h": "04a4dc58893070f7", "c": "\"\"\"\napp/config.py -- Settings for CONSILIUM AI v3.0.\nReads variables from .env via python-dotenv.\n\"\"\"\n\nimport os\nfrom dotenv import load_dotenv\nfrom loguru import logger\n\nload_dotenv()\n\n\nclass Settings:\n APP_NAME: str = \"Consilium AI\"\n APP_VERSION: str = \"3.0.0\"\n DESCRIPTION: str = \"Multi-agent AI Deliberation System\"\n\n # --- Database ---\n DB_PATH: str = \"consilium.db\"\n DATABASE_URL: str = os.getenv(\"DATABASE_URL\", \"sqlite:///./consilium.db\")\n\n # --- AI providers ---\n OPENROUTER_API_KEY: str = os.getenv(\"OPENROUTER_API_KEY\", \"\")\n ANTHROPIC_API_KEY: str = os.getenv(\"ANTHROPIC_API_KEY\", \"\")\n GEMINI_API_KEY: str = os.getenv(\"GEMINI_API_KEY\", \"\")\n GROQ_API_KEY: str = os.getenv(\"GROQ_API_KEY\", \"\")\n\n # --- Ollama (optional, local only) ---\n OLLAMA_URL: str = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n\n # --- SMTP mail ---\n SMTP_SERVER: str = os.getenv(\"SMTP_SERVER\", \"smtp.gmail.com\")\n SMTP_PORT: int = int(os.getenv(\"SMTP_PORT\", \"587\"))\n SMTP_USER: str = os.getenv(\"SMTP_USER\", \"\")\n SMTP_PASS: str = os.getenv(\"SMTP_PASS\", \"\")\n\n # --- Security ---\n SECRET_KEY: str = os.getenv(\"SECRET_KEY\", \"\")\n\n # --- JWT ---\n JWT_ALGORITHM: str = \"HS256\"\n JWT_EXPIRE_HOURS: int = int(os.getenv(\"JWT_EXPIRE_HOURS\", \"24\"))\n\n # --- CORS ---\n CORS_ORIGINS: list = os.getenv(\"CORS_ORIGINS\", \"*\").split(\",\")\n\n\n# -- Singleton instance ──────────────────────────────────────────────────────\nsettings = Settings()\n\n# -- Startup checks (warnings, NOT crashes) ──────────────────────────────────\nif not settings.OPENROUTER_API_KEY:\n logger.warning(\n \"⚠️ OPENROUTER_API_KEY not found. \"\n \"Council mode unavailable. Add key to .env\"\n )\n\nif not settings.SECRET_KEY:\n raise ValueError(\"❌ SECRET_KEY not set in .env!\")\n\n# -- Log active providers ────────────────────────────────────────────────────\n_providers = []\nif settings.OPENROUTER_API_KEY: _providers.append(\"OpenRouter\")\nif settings.ANTHROPIC_API_KEY: _providers.append(\"Claude\")\nif settings.GEMINI_API_KEY: _providers.append(\"Gemini\")\nif settings.GROQ_API_KEY: _providers.append(\"Groq\")\nlogger.info(f\"🔑 Active providers: {', '.join(_providers) or 'none'}\")\nlogger.info(f\"🔐 JWT: algorithm={settings.JWT_ALGORITHM} | expire={settings.JWT_EXPIRE_HOURS}h\")\n"}, "0ab22bd93614d4fff6408b1faa1c01c67b6ad30d": {"t": 1785752962.6443279, "n": 2327, "h": "9b608b4e93a436e5", "c": "\"\"\"\napp/config.py — Настройки CONSILIUM AI v3.0.\nЧитает переменные из .env через python-dotenv.\n\"\"\"\n\nimport os\nfrom dotenv import load_dotenv\nfrom loguru import logger\n\nload_dotenv()\n\n\nclass Settings:\n APP_NAME: str = \"Consilium AI\"\n APP_VERSION: str = \"3.0.0\"\n DESCRIPTION: str = \"Multi-agent AI Deliberation System\"\n\n # --- База данных ---\n DB_PATH: str = \"consilium.db\"\n DATABASE_URL: str = os.getenv(\"DATABASE_URL\", \"sqlite:///./consilium.db\")\n\n # --- AI-провайдеры ---\n OPENROUTER_API_KEY: str = os.getenv(\"OPENROUTER_API_KEY\", \"\")\n ANTHROPIC_API_KEY: str = os.getenv(\"ANTHROPIC_API_KEY\", \"\")\n GEMINI_API_KEY: str = os.getenv(\"GEMINI_API_KEY\", \"\")\n GROQ_API_KEY: str = os.getenv(\"GROQ_API_KEY\", \"\")\n\n # --- Ollama (опционально, только локально) ---\n OLLAMA_URL: str = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n\n # --- SMTP-почта ---\n SMTP_SERVER: str = os.getenv(\"SMTP_SERVER\", \"smtp.gmail.com\")\n SMTP_PORT: int = int(os.getenv(\"SMTP_PORT\", \"587\"))\n SMTP_USER: str = os.getenv(\"SMTP_USER\", \"\")\n SMTP_PASS: str = os.getenv(\"SMTP_PASS\", \"\")\n\n # --- Безопасность ---\n SECRET_KEY: str = os.getenv(\"SECRET_KEY\", \"\")\n\n # --- JWT ---\n JWT_ALGORITHM: str = \"HS256\"\n JWT_EXPIRE_HOURS: int = int(os.getenv(\"JWT_EXPIRE_HOURS\", \"24\"))\n\n # --- CORS ---\n CORS_ORIGINS: list = os.getenv(\"CORS_ORIGINS\", \"*\").split(\",\")\n\n\n# ── Единственный экземпляр ────────────────────────────────────────────────\nsettings = Settings()\n\n# ── Проверки при старте (предупреждения, НЕ краши) ────────────────────────\nif not settings.OPENROUTER_API_KEY:\n logger.warning(\n \"⚠️ OPENROUTER_API_KEY не найден. \"\n \"Council-режим недоступен. Добавьте ключ в .env\"\n )\n\nif not settings.SECRET_KEY:\n raise ValueError(\"❌ SECRET_KEY не установлен в .env!\")\n\n# ── Лог доступных провайдеров ─────────────────────────────────────────────\n_providers = []\nif settings.OPENROUTER_API_KEY: _providers.append(\"OpenRouter\")\nif settings.ANTHROPIC_API_KEY: _providers.append(\"Claude\")\nif settings.GEMINI_API_KEY: _providers.append(\"Gemini\")\nif settings.GROQ_API_KEY: _providers.append(\"Groq\")\nlogger.info(f\"🔑 Активные провайдеры: {', '.join(_providers) or 'нет'}\")\nlogger.info(f\"🔐 JWT: algorithm={settings.JWT_ALGORITHM} | expire={settings.JWT_EXPIRE_HOURS}h\")\n"}}, "core/ai_fallback.py": {"7c7c330c0a8453678a27221e05bf49d756c3c164": {"t": 1786067733.4144418, "n": 28125, "h": "155927acfbb88963", "c": "\"\"\"\nAI Fallback Manager для Consilium AI\nЦепочка: Groq → Bedrock → DeepSeek V4 → OpenRouter → Gemini → Ollama\nGroq добавлен первым — бесплатный, быстрый, свежий ключ.\nBedrock вторым — снимает нагрузку с Groq TPM-лимита (12000/мин), оплачивается\nиз AWS Activate credits.\nDeepSeek V4 третьим — дешевый (~$0.28/1M), MoE-архитектура, OpenAI-совместим.\n\nPer-director temperature/max_tokens (см. core/prompts.py DIRECTOR_CONFIG)\nпередаются через kwargs temperature/max_tokens в call_with_backup и\nприменяются на уровне каждого провайдера, где это технически возможно\n(Groq, DeepSeek). Остальные провайдеры (OpenRouter/Gemini/Ollama) используют\nсвои дефолты, так как это backup-уровни, не основной путь.\n\"\"\"\n\nimport os\nimport asyncio\nimport httpx\nfrom typing import Optional, Dict, Any, Callable\nfrom google import genai\nfrom dotenv import load_dotenv\nimport anthropic\n\nload_dotenv()\n\n\nclass AIFallbackManager:\n \"\"\"\n Провайдеры в порядке приоритета:\n 1. Groq — бесплатный, быстрый (llama3-70b-8192)\n 2. Bedrock — AWS Activate credits, снимает нагрузку с Groq TPM\n 3. DeepSeek V4 — дешево, MoE, OpenAI-совместим\n 4. OpenRouter — платный, все модели\n 5. Gemini — бесплатный лимит\n 6. Ollama — локальный\n \"\"\"\n\n def __init__(self):\n # Groq (новый — приоритет 1)\n self.groq_key = os.getenv(\"GROQ_API_KEY\")\n self.groq_available = bool(self.groq_key)\n self.groq_url = \"https://api.groq.com/openai/v1/chat/completions\"\n self.groq_model = \"llama-3.3-70b-versatile\" # актуальная модель Groq 2026\n if self.groq_available:\n print(\"✅ Groq доступен (приоритет 1)\")\n else:\n print(\"⚠️ GROQ_API_KEY не найден\")\n\n # Amazon Bedrock (приоритет 2 — AWS Activate credits, снимает нагрузку с Groq)\n self.aws_access_key = os.getenv(\"AWS_ACCESS_KEY_ID\")\n self.aws_secret_key = os.getenv(\"AWS_SECRET_ACCESS_KEY\")\n self.aws_region = os.getenv(\"AWS_REGION\", \"us-east-2\")\n self.bedrock_model = os.getenv(\"BEDROCK_MODEL_ID\", \"meta.llama3-3-70b-instruct-v1:0\")\n self.bedrock_available = bool(self.aws_access_key and self.aws_secret_key)\n if self.bedrock_available:\n import boto3\n self.bedrock_client = boto3.client(\n \"bedrock-runtime\",\n region_name=self.aws_region,\n aws_access_key_id=self.aws_access_key,\n aws_secret_access_key=self.aws_secret_key,\n )\n print(\"✅ Bedrock доступен (приоритет 2, AWS Activate credits)\")\n else:\n self.bedrock_client = None\n print(\"⚠️ AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY не найдены — Bedrock пропущен\")\n\n # Claude Haiku 4.5 через Bedrock (bedrock-mantle) — второй шанс на Claude\n # для Synthesizer, когда у прямого ANTHROPIC_API_KEY кончился баланс.\n # Авторизация другая, чем у Llama: не IAM access key, а Bedrock API key\n # (bearer token) — boto3 подхватывает его сам из AWS_BEARER_TOKEN_BEDROCK,\n # явно передавать в client() не нужно. In-Region в us-east-1, поэтому без\n # cross-region \"us.\" префикса (проверено по model card AWS docs).\n # Claude Sonnet 5 не выбран: на момент подключения (07-2026) требует\n # ручного запроса доступа через AWS Sales, самостоятельно не включается.\n self.bedrock_claude_api_key = os.getenv(\"AWS_BEARER_TOKEN_BEDROCK\")\n self.bedrock_claude_region = os.getenv(\"BEDROCK_CLAUDE_REGION\", \"us-east-1\")\n self.bedrock_claude_model = os.getenv(\"BEDROCK_CLAUDE_MODEL_ID\", \"anthropic.claude-haiku-4-5-20251001-v1:0\")\n self.bedrock_claude_available = bool(self.bedrock_claude_api_key)\n if self.bedrock_claude_available:\n import boto3\n self.bedrock_claude_client = boto3.client(\n \"bedrock-runtime\",\n region_name=self.bedrock_claude_region,\n )\n print(\"✅ Claude через Bedrock доступен (Haiku 4.5, для Synthesizer)\")\n else:\n self.bedrock_claude_client = None\n print(\"⚠️ AWS_BEARER_TOKEN_BEDROCK не найден — Claude через Bedrock пропущен\")\n\n # DeepSeek V4 (приоритет 3 — дешево, MoE, OpenAI-совместим)\n self.deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n self.deepseek_available = bool(self.deepseek_key)\n self.deepseek_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.deepseek_model = \"deepseek-v4-pro\"\n if self.deepseek_available:\n print(\"✅ DeepSeek V4 доступен (приоритет 2)\")\n else:\n print(\"⚠️ DEEPSEEK_API_KEY не найден\")\n\n # Claude (для Synthesizer)\n self.claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n if self.claude_key:\n self.claude_client = anthropic.Anthropic(api_key=self.claude_key)\n self.claude_available = True\n print(\"✅ Claude доступна (Synthesizer)\")\n else:\n self.claude_available = False\n print(\"⚠️ ANTHROPIC_API_KEY не найден\")\n\n # Gemini\n gemini_key = os.getenv(\"GEMINI_API_KEY\")\n if gemini_key:\n self.client = genai.Client(api_key=gemini_key)\n self.gemini_available = True\n print(\"✅ Gemini доступна\")\n else:\n self.gemini_available = False\n print(\"⚠️ GEMINI_API_KEY не найден\")\n\n # Ollama\n self.ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n self.ollama_model = \"llama3:8b\"\n self.ollama_available = self._check_ollama()\n\n self.last_provider = None\n\n def _check_ollama(self) -> bool:\n try:\n import urllib.request\n urllib.request.urlopen(f\"{self.ollama_url}/api/tags\", timeout=2.0)\n print(\"✅ Ollama доступна\")\n return True\n except Exception as e:\n print(f\"⚠️ Ollama недоступна: {e}\")\n return False\n\n # ── DeepSeek V4 вызов ────────────────────────────────────────────────\n async def _call_deepseek(self, prompt: str, model: str = None,\n temperature: float = 0.7, max_tokens: int = 2000) -> Dict[str, Any]:\n \"\"\"DeepSeek V4 API (OpenAI-compatible). ~$0.28/1M tokens.\"\"\"\n target = model or self.deepseek_model\n try:\n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n self.deepseek_url,\n headers={\"Authorization\": f\"Bearer {self.deepseek_key}\",\n \"Content-Type\": \"application/json\"},\n json={\n \"model\": target,\n \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n \"max_tokens\": max_tokens,\n \"temperature\": temperature,\n },\n )\n if response.status_code == 200:\n data = response.json()\n content = data[\"choices\"][0][\"message\"][\"content\"]\n tokens = data.get(\"usage\", {}).get(\"total_tokens\", 0)\n self.last_provider = \"deepseek\"\n return {\"success\": True, \"content\": content,\n \"provider\": \"deepseek\", \"model\": target,\n \"tokens\": tokens,\n \"cost_usd\": round(tokens * 0.00000028, 6)}\n else:\n err = response.text[:200]\n return {\"success\": False,\n \"error\": f\"DeepSeek HTTP {response.status_code}: {err}\"}\n except Exception as e:\n return {\"success\": False, \"error\": f\"DeepSeek exception: {str(e)[:100]}\"}\n\n # ── Groq вызов ───────────────────────────────────────────────────────\n async def _call_groq(self, prompt: str, model: str = None,\n temperature: float = 0.7, max_tokens: int = 2000) -> Dict[str, Any]:\n \"\"\"Прямой вызов Groq API (OpenAI-compatible).\n temperature/max_tokens: per-director values from core/prompts.py\n DIRECTOR_CONFIG, passed through call_with_backup -> here.\"\"\"\n target = model or self.groq_model\n try:\n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n self.groq_url,\n headers={\"Authorization\": f\"Bearer {self.groq_key}\",\n \"Content-Type\": \"application/json\"},\n json={\n \"model\": target,\n \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n \"max_tokens\": max_tokens,\n \"temperature\": temperature,\n },\n )\n if response.status_code == 200:\n data = response.json()\n content = data[\"choices\"][0][\"message\"][\"content\"]\n tokens = data.get(\"usage\", {}).get(\"total_tokens\", 0)\n self.last_provider = \"groq\"\n return {\"success\": True, \"content\": content,\n \"provider\": \"groq\", \"model\": target,\n \"tokens\": tokens, \"cost_usd\": 0.0}\n else:\n err = response.text[:200]\n return {\"success\": False,\n \"error\": f\"Groq HTTP {response.status_code}: {err}\"}\n except Exception as e:\n return {\"success\": False, \"error\": f\"Groq exception: {str(e)[:100]}\"}\n\n async def _call_groq_with_system(self, system: str, user_prompt: str, model: str = None,\n temperature: float = 0.35, max_tokens: int = 2500) -> dict:\n \"\"\"Groq call with explicit system + user messages. Used for Chairman.\"\"\"\n target = model or self.groq_model\n try:\n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n self.groq_url,\n headers={\"Authorization\": f\"Bearer {self.groq_key}\",\n \"Content-Type\": \"application/json\"},\n json={\n \"model\": target,\n \"messages\": [\n {\"role\": \"system\", \"content\": system},\n {\"role\": \"user\", \"content\": user_prompt},\n ],\n \"max_tokens\": max_tokens,\n \"temperature\": temperature,\n },\n )\n if response.status_code == 200:\n data = response.json()\n content = data[\"choices\"][0][\"message\"][\"content\"]\n tokens = data.get(\"usage\", {}).get(\"total_tokens\", 0)\n return {\"success\": True, \"content\": content,\n \"provider\": \"groq\", \"model\": target,\n \"tokens\": tokens, \"cost_usd\": 0.0}\n err_body = response.text[:500]\n print(f\"[GROQ ERROR] status={response.status_code} body={err_body}\")\n return {\"success\": False, \"error\": f\"Groq HTTP {response.status_code}: {err_body}\"}\n except Exception as e:\n return {\"success\": False, \"error\": f\"Groq system call error: {str(e)[:100]}\"}\n\n # ── Amazon Bedrock вызов ─────────────────────────────────────────────\n async def _call_bedrock(self, prompt: str, model: str = None,\n temperature: float = 0.7, max_tokens: int = 2000) -> Dict[str, Any]:\n \"\"\"Amazon Bedrock через Converse API (единый формат для всех моделей Bedrock).\"\"\"\n target = model or self.bedrock_model\n try:\n response = await asyncio.to_thread(\n lambda: self.bedrock_client.converse(\n modelId=target,\n messages=[{\"role\": \"user\", \"content\": [{\"text\": prompt}]}],\n inferenceConfig={\"maxTokens\": max_tokens, \"temperature\": temperature},\n )\n )\n content = response[\"output\"][\"message\"][\"content\"][0][\"text\"]\n usage = response.get(\"usage\", {})\n tokens = usage.get(\"inputTokens\", 0) + usage.get(\"outputTokens\", 0)\n self.last_provider = \"bedrock\"\n return {\"success\": True, \"content\": content,\n \"provider\": \"bedrock\", \"model\": target,\n \"tokens\": tokens, \"cost_usd\": 0.0}\n except Exception as e:\n return {\"success\": False, \"error\": f\"Bedrock error: {str(e)[:200]}\"}\n\n # ── Claude через Bedrock (system + user, для Synthesizer) ────────────\n async def _call_bedrock_with_system(self, system: str, user_prompt: str, model: str = None,\n temperature: float = 0.3, max_tokens: int = 1500) -> Dict[str, Any]:\n \"\"\"Bedrock Converse API с отдельным system-промптом. Второй шанс на Claude,\n когда у прямого Anthropic API кончился баланс — списывается с AWS Activate credits.\"\"\"\n target = model or self.bedrock_claude_model\n try:\n response = await asyncio.to_thread(\n lambda: self.bedrock_claude_client.converse(\n modelId=target,\n system=[{\"text\": system}],\n messages=[{\"role\": \"user\", \"content\": [{\"text\": user_prompt}]}],\n inferenceConfig={\"maxTokens\": max_tokens, \"temperature\": temperature},\n )\n )\n content = response[\"output\"][\"message\"][\"content\"][0][\"text\"]\n usage = response.get(\"usage\", {})\n tokens = usage.get(\"inputTokens\", 0) + usage.get(\"outputTokens\", 0)\n self.last_provider = \"bedrock-claude\"\n return {\"success\": True, \"content\": content,\n \"provider\": \"bedrock-claude\", \"model\": target,\n \"tokens\": tokens, \"cost_usd\": 0.0}\n except Exception as e:\n return {\"success\": False, \"error\": f\"Bedrock Claude error: {str(e)[:200]}\"}\n\n # ── Main fallback chain ───────────────────────────────────────────────────\n async def call_with_backup(self, primary_func: Callable, *args,\n temperature: float = 0.7, max_tokens: int = 2000,\n **kwargs) -> Dict[str, Any]:\n \"\"\"Цепочка: Groq → Bedrock → DeepSeek V4 → OpenRouter → Gemini → Ollama\n\n temperature/max_tokens: per-director values from core/prompts.py\n DIRECTOR_CONFIG (council.py passes these in based on the director role).\n Applied directly to Groq, Bedrock and DeepSeek calls; OpenRouter/Gemini/Ollama\n are backup levels and keep their own internal defaults.\n \"\"\"\n\n # Если локальная модель (без '/') → сразу Ollama\n spec_arg = args[0] if args else None\n model_name = getattr(spec_arg, 'model', '') if spec_arg else ''\n is_local_model = bool(model_name) and '/' not in model_name\n\n if is_local_model:\n print(f\"🔄 Локальная модель ({model_name}): → Ollama\")\n prompt = self._extract_prompt(args, kwargs)\n if self.ollama_available:\n result = await self.call_ollama_direct(prompt, model=model_name)\n if result.get(\"success\"):\n return result\n # Fallback на Groq если Ollama упала\n if self.groq_available:\n result = await self._call_groq(prompt, temperature=temperature, max_tokens=max_tokens)\n if result.get(\"success\"):\n print(\" ✅ Groq fallback для local model\")\n return result\n return {\"success\": False, \"error\": \"Все провайдеры недоступны\",\n \"provider\": \"none\", \"content\": \"[Ошибка: все провайдеры недоступны]\"}\n\n # ── УРОВЕНЬ 1: Groq ─────────────────────────────────────────────\n if self.groq_available:\n try:\n prompt = self._extract_prompt(args, kwargs)\n result = await self._call_groq(prompt, temperature=temperature, max_tokens=max_tokens)\n if result.get(\"success\"):\n print(f\"✅ Groq: {len(result.get('content',''))} chars (temp={temperature})\")\n return result\n else:\n print(f\"⚠️ Groq failed: {result.get('error','')[:80]}\")\n except Exception as e:\n print(f\"⚠️ Groq exception: {str(e)[:80]}\")\n\n # ── УРОВЕНЬ 2: Amazon Bedrock ────────────────────────────────────\n if self.bedrock_available:\n try:\n prompt = self._extract_prompt(args, kwargs)\n result = await self._call_bedrock(prompt, temperature=temperature, max_tokens=max_tokens)\n if result.get(\"success\"):\n print(f\"✅ Bedrock: {len(result.get('content',''))} chars (temp={temperature})\")\n return result\n else:\n print(f\"⚠️ Bedrock failed: {result.get('error','')[:80]}\")\n except Exception as e:\n print(f\"⚠️ Bedrock exception: {str(e)[:80]}\")\n\n # ── УРОВЕНЬ 3: DeepSeek V4 ───────────────────────────────────────\n if self.deepseek_available:\n try:\n prompt = self._extract_prompt(args, kwargs)\n result = await self._call_deepseek(prompt, temperature=temperature, max_tokens=max_tokens)\n if result.get(\"success\"):\n print(f\"✅ DeepSeek: {len(result.get('content',''))} chars (temp={temperature})\")\n return result\n else:\n print(f\"⚠️ DeepSeek failed: {result.get('error','')[:80]}\")\n except Exception as e:\n print(f\"⚠️ DeepSeek exception: {str(e)[:80]}\")\n\n # ── УРОВЕНЬ 4: OpenRouter ────────────────────────────────────────\n try:\n result = await asyncio.wait_for(primary_func(*args, **kwargs), timeout=60.0)\n if result and hasattr(result, '__dataclass_fields__'):\n if not result.error:\n self.last_provider = \"openrouter\"\n return {\"success\": True, \"content\": result.content,\n \"provider\": \"openrouter\", \"model\": result.model,\n \"tokens\": getattr(result, 'tokens_in', 0) + getattr(result, 'tokens_out', 0),\n \"cost_usd\": getattr(result, 'cost_usd', 0)}\n else:\n raise Exception(f\"OpenRouter error: {result.error}\")\n if result and isinstance(result, dict) and result.get(\"success\", True):\n self.last_provider = \"openrouter\"\n return {\"success\": True,\n \"content\": result.get(\"content\") or result.get(\"text\"),\n \"provider\": \"openrouter\", \"model\": result.get(\"model\", \"unknown\"),\n \"tokens\": result.get(\"tokens_used\", 0) or result.get(\"tokens\", 0),\n \"cost_usd\": result.get(\"cost_usd\", 0)}\n raise Exception(\"OpenRouter returned error response\")\n except asyncio.TimeoutError:\n print(\"⏱️ OpenRouter timeout (60s)\")\n except Exception as e:\n print(f\"⚠️ OpenRouter failed: {str(e)[:100]}\")\n\n # ── УРОВЕНЬ 5: Gemini ────────────────────────────────────────────\n if self.gemini_available:\n try:\n prompt = self._extract_prompt(args, kwargs)\n response = self.client.models.generate_content(\n model=\"gemini-2.0-flash\", contents=prompt[:8000])\n self.last_provider = \"gemini\"\n print(\"✅ Gemini backup\")\n return {\"success\": True, \"content\": response.text,\n \"provider\": \"gemini\", \"model\": \"gemini-2.0-flash\",\n \"tokens\": self._estimate_tokens(prompt, response.text),\n \"cost_usd\": 0.0}\n except Exception as e:\n print(f\"⚠️ Gemini failed: {str(e)[:100]}\")\n\n # ── УРОВЕНЬ 6: Ollama ────────────────────────────────────────────\n if self.ollama_available:\n try:\n prompt = self._extract_prompt(args, kwargs)\n print(f\"🔄 Переключение на Ollama ({self.ollama_model})...\")\n result = await self.call_ollama_direct(prompt)\n if result.get(\"success\"):\n print(\"✅ Ollama successful\")\n return result\n else:\n raise Exception(result.get(\"error\", \"Unknown Ollama error\"))\n except Exception as e:\n print(f\"⚠️ Ollama failed: {str(e)[:120]}\")\n\n return {\"success\": False,\n \"error\": \"All providers failed (Groq → Bedrock → DeepSeek → OpenRouter → Gemini → Ollama)\",\n \"provider\": \"none\",\n \"content\": \"[Ошибка: все провайдеры недоступны. Проверьте GROQ_API_KEY в .env]\"}\n\n # ── Synthesizer (Claude → Claude via Bedrock → Groq → Gemini → Ollama) ──\n async def call_claude_for_synthesis(self, system_prompt: str, user_prompt: str) -> Dict[str, Any]:\n if not user_prompt or len(user_prompt.strip()) < 50:\n return {\"success\": False, \"error\": \"Empty context\", \"content\": \"\"}\n\n # Claude\n if self.claude_available:\n try:\n response = await asyncio.to_thread(\n lambda: self.claude_client.messages.create(\n model=\"claude-3-5-sonnet-20241022\",\n max_tokens=1500, system=system_prompt,\n messages=[{\"role\": \"user\", \"content\": user_prompt[:4000]}],\n temperature=0.3))\n self.last_provider = \"claude\"\n return {\"success\": True, \"content\": response.content[0].text,\n \"provider\": \"claude\", \"model\": \"claude-3-5-sonnet\",\n \"tokens\": response.usage.input_tokens + response.usage.output_tokens,\n \"cost_usd\": 0.003}\n except Exception as e:\n err_str = str(e)\n print(f\"⚠️ Claude failed: {err_str[:100]}\")\n if any(x in err_str.lower() for x in [\"credit\", \"billing\", \"400\", \"invalid_request\"]):\n self.claude_available = False\n\n # Claude через Bedrock — второй шанс на Claude без баланса Anthropic\n if self.bedrock_claude_available:\n try:\n result = await self._call_bedrock_with_system(system_prompt, user_prompt[:4000])\n if result.get(\"success\"):\n print(\"✅ Bedrock Claude synthesis\")\n return result\n else:\n print(f\"⚠️ Bedrock Claude synthesis failed: {result.get('error','')[:80]}\")\n except Exception as e:\n print(f\"⚠️ Bedrock Claude synthesis exception: {str(e)[:80]}\")\n\n # Groq для синтеза\n if self.groq_available:\n try:\n combined = f\"{system_prompt[:500]}\\n\\n{user_prompt[:3000]}\"\n result = await self._call_groq(combined, model=\"llama3-70b-8192\")\n if result.get(\"success\"):\n print(\"✅ Groq synthesis backup\")\n return result\n except Exception as e:\n print(f\"⚠️ Groq synthesis failed: {str(e)[:80]}\")\n\n # Gemini\n if self.gemini_available:\n try:\n combined = f\"{system_prompt}\\n\\n{user_prompt[:3000]}\"\n response = self.client.models.generate_content(\n model=\"gemini-2.0-flash\", contents=combined)\n self.last_provider = \"gemini\"\n print(\"✅ Gemini synthesis backup\")\n return {\"success\": True, \"content\": response.text,\n \"provider\": \"gemini\", \"model\": \"gemini-2.0-flash\",\n \"tokens\": self._estimate_tokens(user_prompt, response.text),\n \"cost_usd\": 0.0}\n except Exception as e:\n print(f\"⚠️ Gemini synthesis failed: {str(e)[:100]}\")\n\n # Ollama\n if self.ollama_available:\n try:\n return await self.call_ollama_direct(user_prompt[:2000], fast_mode=False)\n except Exception as e:\n print(f\"⚠️ Ollama synthesis failed: {str(e)[:100]}\")\n\n return {\"success\": False, \"error\": \"All synthesis providers failed\",\n \"content\": \"[Синтез недоступен]\"}\n\n # ── Ollama direct ─────────────────────────────────────────────────────\n async def call_ollama_direct(self, prompt: str, model: str = None,\n fast_mode: bool = False) -> Dict[str, Any]:\n target_model = model if model else self.ollama_model\n num_predict = 350 if fast_mode else 512\n num_ctx = 2048 if fast_mode else 2048 # меньше контекст = быстрее\n timeout_sec = 60.0 if fast_mode else 90.0 # быстрый fail → следующий провайдер\n\n payload = {\n \"model\": target_model,\n \"prompt\": str(prompt)[:4000], # ограничиваем длину\n \"stream\": False,\n \"options\": {\"num_predict\": num_predict, \"temperature\": 0.7,\n \"num_ctx\": num_ctx, \"num_thread\": 4},\n }\n try:\n print(f\" 🔄 Ollama: {target_model} (predict={num_predict})...\")\n async with httpx.AsyncClient(\n timeout=httpx.Timeout(timeout_sec, read=timeout_sec, connect=10.0)\n ) as client:\n response = await client.post(f\"{self.ollama_url}/api/generate\", json=payload)\n if response.status_code == 200:\n data = response.json()\n content = data.get(\"response\", \"\").strip()\n if not content:\n return {\"success\": False, \"error\": \"Ollama returned empty response\"}\n return {\"success\": True, \"content\": content, \"model\": target_model,\n \"provider\": \"ollama\",\n \"tokens\": data.get(\"eval_count\", 0), \"cost_usd\": 0.0}\n else:\n return {\"success\": False,\n \"error\": f\"Ollama HTTP {response.status_code}: {response.text[:100]}\"}\n except httpx.ConnectError:\n return {\"success\": False, \"error\": \"Ollama не запущена (Connection refused)\"}\n except httpx.TimeoutException:\n return {\"success\": False, \"error\": f\"Ollama timeout ({timeout_sec}s)\"}\n except Exception as e:\n return {\"success\": False, \"error\": f\"{type(e).__name__}: {str(e)[:100]}\"}\n\n # ── Helpers ───────────────────────────────────────────────────────────\n def _extract_prompt(self, args, kwargs) -> str:\n \"\"\"Извлекает текст промпта из args/kwargs любого формата.\"\"\"\n # Именованный аргумент\n if 'prompt' in kwargs:\n return str(kwargs['prompt'])\n # messages как kwarg\n if 'messages' in kwargs:\n msgs = kwargs['messages']\n if isinstance(msgs, list):\n return \"\\n\".join(m.get('content', '') for m in msgs\n if isinstance(m, dict))\n return str(msgs)\n # args[1] — второй позиционный (список messages или строка)\n if args and len(args) >= 2:\n arg1 = args[1]\n if isinstance(arg1, list):\n # Список dict messages\n return \"\\n\".join(\n m.get('content', '') for m in arg1\n if isinstance(m, dict)\n )\n return str(arg1)\n # args[0] как строка\n if args and isinstance(args[0], str):\n return args[0]\n return str(args[0]) if args else \"\"\n\n def _estimate_tokens(self, prompt: str, response: str) -> int:\n return (len(prompt) + len(response)) // 4\n\n\n# Глобальный экземпляр\nfallback_manager = AIFallbackManager()\n"}}}} |