Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/autoalexai-sketch_consilium-ai.json
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
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.
2026-08-07 12:53:47 +08:00

1 line
49 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}