{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "YVQ4X5N3KB0U"
      },
      "source": [
        "# StudyWise - endpoint de IA no Google Colab\n",
        "\n",
        "Este notebook inicia um modelo local com API compatível com OpenAI para uso temporário no Gerador de Conteúdo. O runtime e a URL deixam de funcionar quando o Colab desconecta. Não use este endpoint como infraestrutura permanente de produção.\n",
        "\n",
        "**Antes de executar:** ative uma GPU em `Ambiente de execução > Alterar tipo de ambiente de execução`. Se você executou uma versão antiga deste notebook, escolha `Desconectar e excluir ambiente de execução` uma vez e conecte novamente para remover pacotes incompatíveis instalados anteriormente. Depois, execute as células em ordem."
      ],
      "id": "YVQ4X5N3KB0U"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "7JyrtPyQKB0a"
      },
      "outputs": [],
      "source": [
        "!nvidia-smi\n",
        "import shutil, subprocess\n",
        "\n",
        "assert shutil.which('nvidia-smi'), 'Ative uma GPU em Ambiente de execução > Alterar tipo de ambiente de execução.'\n",
        "gpu_name = subprocess.check_output(\n",
        "    ['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'], text=True\n",
        ").splitlines()[0].strip()\n",
        "gpu_memory_mb = int(subprocess.check_output(\n",
        "    ['nvidia-smi', '--query-gpu=memory.total', '--format=csv,noheader,nounits'], text=True\n",
        ").splitlines()[0].strip())\n",
        "print(f'GPU: {gpu_name} ({gpu_memory_mb / 1024:.1f} GB de VRAM)')"
      ],
      "id": "7JyrtPyQKB0a"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "tOx59vSwKB0d"
      },
      "outputs": [],
      "source": [
        "import secrets\n",
        "\n",
        "# O modelo é escolhido pela VRAM. Você pode alterar manualmente depois, se necessário.\n",
        "MODEL_ID = (\n",
        "    'Qwen/Qwen2.5-14B-Instruct-AWQ'\n",
        "    if gpu_memory_mb >= 20_000\n",
        "    else 'Qwen/Qwen2.5-7B-Instruct-AWQ'\n",
        ")\n",
        "MAX_MODEL_LEN = 32768\n",
        "PORT = 8000\n",
        "API_KEY = 'sw_colab_' + secrets.token_urlsafe(48)\n",
        "print('Modelo selecionado:', MODEL_ID)\n",
        "print('Chave gerada para este runtime:', API_KEY)\n",
        "print('Guarde esta chave somente no admin da StudyWise e não a compartilhe.')"
      ],
      "id": "tOx59vSwKB0d"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "aRdPAgCRKB0e"
      },
      "outputs": [],
      "source": [
        "import os, pathlib, shutil, subprocess, sys\n",
        "\n",
        "# O vLLM fica isolado das bibliotecas internas do Colab. Isso evita conflitos de\n",
        "# requests, protobuf, numba e, principalmente, wheels da CUDA incompatíveis.\n",
        "VENV_DIR = pathlib.Path('/content/studywise-vllm-env')\n",
        "if VENV_DIR.exists():\n",
        "    shutil.rmtree(VENV_DIR)\n",
        "\n",
        "subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'uv'], check=True)\n",
        "UV = shutil.which('uv')\n",
        "assert UV, 'Não foi possível instalar o uv.'\n",
        "subprocess.run([UV, 'venv', str(VENV_DIR), '--python', sys.executable, '--seed'], check=True)\n",
        "VENV_PYTHON = str(VENV_DIR / 'bin/python')\n",
        "VLLM_BIN = str(VENV_DIR / 'bin/vllm')\n",
        "\n",
        "install_command = [\n",
        "    UV, 'pip', 'install', '--python', VENV_PYTHON,\n",
        "    'vllm', 'huggingface_hub', 'requests==2.32.4',\n",
        "    '--torch-backend=auto',\n",
        "]\n",
        "print('Instalando vLLM com o backend CUDA compatível. Isso pode levar alguns minutos...')\n",
        "subprocess.run(install_command, check=True)\n",
        "\n",
        "validation = subprocess.run(\n",
        "    [VENV_PYTHON, '-c', (\n",
        "        \"import torch, vllm; \"\n",
        "        \"assert torch.cuda.is_available(); \"\n",
        "        \"print('vLLM', vllm.__version__, '| PyTorch', torch.__version__, \"\n",
        "        \"'| CUDA', torch.version.cuda, '| GPU', torch.cuda.get_device_name(0))\"\n",
        "    )],\n",
        "    text=True, capture_output=True,\n",
        ")\n",
        "if validation.returncode != 0:\n",
        "    raise RuntimeError('Falha ao validar o ambiente isolado:\\n' + validation.stderr[-5000:])\n",
        "print(validation.stdout.strip())\n",
        "\n",
        "subprocess.run([\n",
        "    'wget', '-q', '-O', '/content/cloudflared',\n",
        "    'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64',\n",
        "], check=True)\n",
        "os.chmod('/content/cloudflared', 0o755)\n",
        "print('Dependências instaladas e validadas.')"
      ],
      "id": "aRdPAgCRKB0e"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "-7-1usMbKB0f"
      },
      "outputs": [],
      "source": [
        "import socket, subprocess, time\n",
        "\n",
        "# Encerra processos deste notebook antes de reiniciar a API.\n",
        "for process_name in ('server', 'tunnel'):\n",
        "    process = globals().get(process_name)\n",
        "    if process is not None and process.poll() is None:\n",
        "        process.terminate()\n",
        "        try:\n",
        "            process.wait(timeout=10)\n",
        "        except subprocess.TimeoutExpired:\n",
        "            process.kill()\n",
        "\n",
        "server_log_path = '/content/studywise-vllm.log'\n",
        "server_log = open(server_log_path, 'w')\n",
        "server_command = [\n",
        "    VLLM_BIN, 'serve', MODEL_ID,\n",
        "    '--served-model-name', MODEL_ID,\n",
        "    '--host', '0.0.0.0',\n",
        "    '--port', str(PORT),\n",
        "    '--api-key', API_KEY,\n",
        "    '--dtype', 'auto',\n",
        "    '--gpu-memory-utilization', '0.88',\n",
        "    '--max-model-len', str(MAX_MODEL_LEN),\n",
        "]\n",
        "server = subprocess.Popen(server_command, stdout=server_log, stderr=subprocess.STDOUT)\n",
        "\n",
        "print('Carregando o modelo. A primeira execução pode levar até 15 minutos...')\n",
        "deadline = time.time() + 900\n",
        "while time.time() < deadline:\n",
        "    if server.poll() is not None:\n",
        "        server_log.flush()\n",
        "        raise RuntimeError(open(server_log_path).read()[-8000:])\n",
        "    with socket.socket() as sock:\n",
        "        sock.settimeout(1)\n",
        "        if sock.connect_ex(('127.0.0.1', PORT)) == 0:\n",
        "            break\n",
        "    time.sleep(5)\n",
        "else:\n",
        "    server.terminate()\n",
        "    raise TimeoutError('O modelo não iniciou em 15 minutos. Consulte /content/studywise-vllm.log.')\n",
        "print('Servidor vLLM pronto em http://127.0.0.1:8000/v1.')"
      ],
      "id": "-7-1usMbKB0f"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "gDFTRUWzKB0g"
      },
      "outputs": [],
      "source": [
        "import re\n",
        "from IPython.display import Markdown, display\n",
        "\n",
        "old_tunnel = globals().get('tunnel')\n",
        "if old_tunnel is not None and old_tunnel.poll() is None:\n",
        "    old_tunnel.terminate()\n",
        "    old_tunnel.wait(timeout=10)\n",
        "\n",
        "tunnel_log = open('/content/studywise-cloudflared.log', 'w')\n",
        "tunnel = subprocess.Popen(\n",
        "    ['/content/cloudflared', 'tunnel', '--url', f'http://127.0.0.1:{PORT}', '--no-autoupdate'],\n",
        "    stdout=tunnel_log, stderr=subprocess.STDOUT\n",
        ")\n",
        "public_url = ''\n",
        "deadline = time.time() + 90\n",
        "while time.time() < deadline and not public_url:\n",
        "    time.sleep(2)\n",
        "    tunnel_log.flush()\n",
        "    content = open('/content/studywise-cloudflared.log').read()\n",
        "    match = re.search(r'https://[a-z0-9-]+\\.trycloudflare\\.com', content)\n",
        "    if match:\n",
        "        public_url = match.group(0)\n",
        "if not public_url:\n",
        "    raise RuntimeError(open('/content/studywise-cloudflared.log').read()[-3000:])\n",
        "\n",
        "print('CADASTRE NO ADMIN DA STUDYWISE')\n",
        "print('Provedor: Google Colab')\n",
        "print('Endpoint:', public_url + '/v1')\n",
        "print('Modelo:', MODEL_ID)\n",
        "print('Chave da API:', API_KEY)\n",
        "print('Copie os três valores acima para a configuração do Google Colab no admin.')\n",
        "display(Markdown(f'''\n",
        "## Configuração pronta para copiar\n",
        "\n",
        "**Endpoint compatível com OpenAI**\n",
        "```text\n",
        "{public_url}/v1\n",
        "```\n",
        "\n",
        "**Modelo ativo**\n",
        "```text\n",
        "{MODEL_ID}\n",
        "```\n",
        "\n",
        "**Chave da API**\n",
        "```text\n",
        "{API_KEY}\n",
        "```\n",
        "'''))"
      ],
      "id": "gDFTRUWzKB0g"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "JvgznjzWKB0h"
      },
      "outputs": [],
      "source": [
        "import json, urllib.request\n",
        "\n",
        "payload = json.dumps({\n",
        "        'model': MODEL_ID,\n",
        "        'messages': [{'role': 'user', 'content': 'Responda apenas: endpoint operacional'}],\n",
        "        'max_tokens': 20,\n",
        "        'temperature': 0,\n",
        "}).encode('utf-8')\n",
        "request = urllib.request.Request(\n",
        "    public_url + '/v1/chat/completions',\n",
        "    data=payload,\n",
        "    headers={'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json'},\n",
        "    method='POST',\n",
        ")\n",
        "with urllib.request.urlopen(request, timeout=180) as response:\n",
        "    result = json.loads(response.read().decode('utf-8'))\n",
        "print(result['choices'][0]['message']['content'])\n",
        "print('Teste concluído: o endpoint está operacional.')"
      ],
      "id": "JvgznjzWKB0h"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "vyQiGxBzKB0i"
      },
      "outputs": [],
      "source": [
        "print('Endpoint ativo. Mantenha esta célula executando enquanto usar o Colab na StudyWise.')\n",
        "while server.poll() is None and tunnel.poll() is None:\n",
        "    time.sleep(30)\n",
        "raise RuntimeError('O servidor ou o túnel foi encerrado. Execute novamente o notebook e atualize a URL no admin.')"
      ],
      "id": "vyQiGxBzKB0i"
    }
  ],
  "metadata": {
    "accelerator": "GPU",
    "colab": {
      "gpuType": "T4",
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}