Cómo construir un plugin de Chrome usando Novita AI Sandbox

Cómo construir un plugin de Chrome usando Novita AI Sandbox

Imagina navegar por sitios de documentación o tutoriales de código y nunca sentirte solo. En lugar de recorrerlo todo por tu cuenta, tienes un asistente de IA que te sigue de página en página. No está ligado a una sola página web. Siempre está ahí contigo, listo para ayudar. Puede ejecutar de forma segura el código que encuentras en línea, ofrecer explicaciones y brindarte información en el momento que la necesitas.

Entonces, ¿cómo haces realidad esta experiencia? Creando una extensión de navegador. Esta extensión incluirá un agente de IA con el que el usuario pueda chatear, y el agente tendrá acceso a una máquina sandbox segura donde pueda ejecutar código y realizar otras operaciones de forma segura.

Para crear este sistema, construiremos una extensión de Chrome, usaremos un modelo de Novita capaz de usar herramientas e integraremos el Sandbox de Novita como el entorno de ejecución seguro para el agente. En este artículo, recorreremos el proceso completo de su construcción.

screenshoot of code aassistant

Al final de este tutorial, aprenderás:

  • Cómo construir una extensión de Chrome que se integra con un agente de IA
  • Cómo utilizar los LLM agentivos de Novita
  • Cómo configurar Novita Sandbox como un entorno seguro para el agente del navegador
  • Cómo hacer que la extensión se comunique en tiempo real con el agente

Un Sandbox: La única herramienta que necesitas

La extensión de Chrome que estamos construyendo depende de un agente de IA para ayudar al usuario. Dado que esta extensión está pensada para funcionar como asistente de codificación, el agente necesita la capacidad de ejecutar código, crear archivos, inspeccionar resultados y realizar todas las tareas típicas que un desarrollador podría hacer. Podrías esperar que requiriera una larga lista de herramientas para lograrlo, pero en realidad solo necesita una: un sandbox.

Un sandbox le da al agente acceso a un entorno Linux donde puede ejecutar comandos, crear y modificar archivos, y llevar a cabo cualquier operación que normalmente harías en una terminal. Para este proyecto, usaremos el Novita Sandbox.

Para configurarlo, primero instala el paquete Novita Sandbox:

pip install novita-sandbox

A continuación, establece la variable de entorno NOVITA_API_KEY con tu clave de API. Una vez hecho esto, puedes crear y usar un sandbox de la siguiente manera:

from novita_sandbox.code_interpreter import Sandbox

sandbox = Sandbox.create()

result = sandbox.commands.run('ls -l')
print(result)

sandbox.kill()

Este fragmento crea un sandbox, ejecuta el comando ls -l, imprime la salida y luego apaga el sandbox. Este flujo de trabajo simple es la base de cómo nuestro asistente de navegador aprovechará el sandbox para ayudar a los usuarios.

Ahora apliquemos este principio a la extensión completa.

Arquitectura del agente asistente del navegador

La arquitectura de este proyecto sigue un modelo cliente-servidor. La extensión de Chrome actúa como cliente, mientras que un servidor back-end dedicado aloja tanto al agente de IA como al entorno sandbox.

La extensión se comunica con el servidor a través de una conexión WebSocket. Esto permite mensajería bidireccional en tiempo real, de modo que las solicitudes del usuario y las respuestas del agente fluyan instantáneamente sin demoras notables. El servidor, a su vez, se comunica con las APIs de Novita que incluyen el endpoint del modelo y el servicio sandbox.

Juntos, la extensión y el back-end forman un asistente de navegador inteligente capaz de ejecutar código de forma segura, procesar información rápidamente y proporcionar explicaciones útiles directamente en la experiencia de navegación del usuario.

Construyendo la extensión

Ahora que entendemos la arquitectura general, podemos comenzar a implementar la extensión en sí. Empezaremos con el servidor del plugin.

El servidor de la extensión

El servidor de la extensión es un servicio WebSocket simple con un único endpoint /ws. Este endpoint recibe mensajes del usuario y devuelve las respuestas del LLM en tiempo real. También maneja las llamadas a herramientas invocando el sandbox cada vez que el agente necesita ejecutar código o realizar una operación.

Dependencias

El servidor depende de tres bibliotecas principales:

  • FastAPI: El framework HTTP que proporciona la implementación de WebSocket
  • OpenAI: El SDK utilizado para comunicarse con los modelos de Novita
  • Novita Sandbox: El entorno seguro donde se ejecuta el código de manera segura

Instálalos con:

pip install novita-sandbox "fastapi[standard]" openai

Establece tu clave de API de Novita como variable de entorno:

export NOVITA_API_KEY = sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Escribiendo el código del servidor

Comienza importando los módulos requeridos:

import os
import json
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from novita_sandbox.code_interpreter import Sandbox
from openai import OpenAI

Inicializando el cliente LLM

A continuación, crea un cliente de OpenAI que apunte a la API de Novita. En este ejemplo usamos el modelo llama-3.3-70b-instruct, pero cualquier modelo de Novita que soporte llamadas a herramientas funcionará.

client = OpenAI(
   base_url="https://api.novita.ai/openai",
   api_key=os.environ["NOVITA_API_KEY"],
)

model = "meta-llama/llama-3.3-70b-instruct"

Definiendo el esquema de herramientas

El agente usará cuatro herramientas, cada una interactuando con el sandbox:

  • read_file: Lee el contenido de un archivo
  • write_file: Crea y escribe en un solo archivo
  • write_files: Crea y escribe en múltiples archivos
  • run_commands: Ejecuta comandos de shell dentro del sandbox

Aquí está el esquema completo de herramientas:

tools = [
   {
       "type": "function",
       "function": {
           "name": "read_file",
           "description": "Read contents of a file inside the sandbox",
           "parameters": {
               "type": "object",
               "properties": {"path": {"type": "string"}},
               "required": ["path"],
           },
       },
   },
   {
       "type": "function",
       "function": {
           "name": "write_file",
           "description": "Write a single file inside the sandbox",
           "parameters": {
               "type": "object",
               "properties": {
                   "path": {"type": "string"},
                   "data": {"type": "string"},
               },
               "required": ["path", "data"],
           },
       },
   },
   {
       "type": "function",
       "function": {
           "name": "write_files",
           "description": "Write multiple files inside the sandbox",
           "parameters": {
               "type": "object",
               "properties": {
                   "files": {
                       "type": "array",
                       "items": {
                           "type": "object",
                           "properties": {
                               "path": {"type": "string"},
                               "data": {"type": "string"},
                           },
                           "required": ["path", "data"],
                       },
                   }
               },
               "required": ["files"],
           },
       },
   },
   {
       "type": "function",
       "function": {
           "name": "run_commands",
           "description": "Run a shell command inside the sandbox working directory",
           "parameters": {
               "type": "object",
               "properties": {
                   "command": {"type": "string"},
               },
               "required": ["command"],
           },
       },
   },
]

Configurando el servidor HTTP

Configura FastAPI y habilita CORS para que la extensión de Chrome pueda hacer solicitudes al servidor.

app = FastAPI()

app.add_middleware(
   CORSMiddleware,
   allow_origins=["*"],
   allow_methods=["*"],
   allow_headers=["*"],
)

Implementando las herramientas

Define una función manejadora que implemente cada herramienta y enrute las llamadas a herramientas del agente hacia el sandbox:

def make_tool_handlers(sandbox):
   def read_file(path: str):
       print(f"[LOG] read_file called with path: {path}")
       try:
           content = sandbox.files.read(path)
           print(f"[LOG] read_file result: {content}")
           return content
       except Exception as e:
           return f"Error reading file: {e}"

   def write_file(path: str, data: str):
       print(f"[LOG] write_file called with path: {path}")
       try:
           sandbox.files.write(path, data)
           return f"File created successfully at {path}"
       except Exception as e:
           return f"Error writing file: {e}"

   def write_files(files: list):
       print(f"[LOG] write_files called with {len(files)} files")
       try:
           sandbox.files.write_files(files)
           return f"{len(files)} file(s) created successfully"
       except Exception as e:
           return f"Error writing multiple files: {e}"

   def run_commands(command: str):
       print(f"[LOG] run_commands called with command: {command}")
       try:
           result = sandbox.commands.run(command)
           return result.stdout
       except Exception as e:
           return f"Error running command: {e}"

   return {
       "read_file": read_file,
       "write_file": write_file,
       "write_files": write_files,
       "run_commands": run_commands,
   }

Creando el endpoint WebSocket

Ahora implementemos el endpoint WebSocket. Cuando el usuario se conecta al endpoint, se crea una nueva instancia de sandbox.

Toda la comunicación entre el usuario y el agente fluye a través de esta conexión. Si el usuario le pide al agente que use una herramienta, el agente selecciona y ejecuta la herramienta adecuada a través de la función manejadora.

Cuando se cierra la conexión, el sandbox se termina.

@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
   await ws.accept()
   print("\
[WS] Client connected")

   # Create sandbox per connection
   sandbox = Sandbox.create(timeout=1200)
   print("[WS] Sandbox created")

   tools_exec = make_tool_handlers(sandbox)
   messages = []  # persistent inside this websocket

   try:
       while True:
           data = await ws.receive_text()
           print(f"[WS] Received message: {data}")

           # Add user message
           messages.append({"role": "user", "content": data})

           # LLM call
           response = client.chat.completions.create(
               model=model,
               messages=messages,
               tools=tools,
           )

           assistant_msg = response.choices[0].message
           messages.append(assistant_msg)

           # If LLM wants to call tools
           if assistant_msg.tool_calls:
               print(f"[WS] Assistant requested {len(assistant_msg.tool_calls)} tool call(s).")

               results = []

               for tool_call in assistant_msg.tool_calls:
                   fn_name = tool_call.function.name
                   fn_args = json.loads(tool_call.function.arguments)

                   print(f"[WS] Tool call: {fn_name} args={fn_args}")

                   if fn_name in tools_exec:
                       result = tools_exec[fn_name](**fn_args)
                   else:
                       result = f"Error: Unknown tool {fn_name}"

                   results.append(result)

                   messages.append({
                       "tool_call_id": tool_call.id,
                       "role": "tool",
                       "content": str(result),
                   })

               # Follow-up model call
               follow_up = client.chat.completions.create(
                   model=model,
                   messages=messages,
               )
               final_answer = follow_up.choices[0].message
               messages.append(final_answer)

               await ws.send_json({
                   "reply": final_answer.content,
                   "tool_output": results,
               })

           else:
               # Simple model text output
               await ws.send_json({"reply": assistant_msg.content})

   except WebSocketDisconnect:
       print("[WS] Client disconnected")

   finally:
       sandbox.kill()
       print("[WS] Sandbox terminated")

Ejecutando el servidor

Finalmente, usa Uvicorn para lanzar el servicio:

if __name__ == "__main__":
   uvicorn.run(app, host="0.0.0.0", port=8000)

Esto completa el componente del servidor.

La extensión

La extensión es la interfaz con la que el usuario interactúa. Consiste en un pequeño conjunto de archivos que trabajan juntos para ejecutarse en cualquier página web. Una vez activada, el usuario puede comunicarse con el servidor de la extensión en tiempo real directamente desde la página que está navegando.

La extensión incluye los siguientes archivos:

  • manifest.json: Define la configuración y los permisos de la extensión
  • background.js: Contiene la lógica del service worker y maneja las acciones del menú contextual
  • content.js: Gestiona las interacciones dentro de la página y muestra el diálogo del asistente
  • styles.css: Proporciona el estilo para la ventana del asistente en la página

Cada archivo tiene una responsabilidad distinta, y juntos forman una extensión completa y funcional.

Cómo funciona la extensión

Antes de implementar los archivos, echemos un vistazo rápido a cómo funciona la extensión desde la perspectiva del usuario:

screenshoot of agent sandbox plugin

  1. El usuario hace clic derecho en cualquier página web y aparece el menú contextual.
  2. Selecciona “Agent Sandbox” del menú.
  3. La extensión abre el diálogo del asistente en la página.
  4. El usuario hace clic en Connect para establecer una conexión con el servidor de la extensión.
  5. Una vez establecida la conexión, el usuario puede empezar a escribir directamente en el cuadro de mensajes.
  6. Después de escribir su mensaje, simplemente hace clic en Send para enviarlo al servidor.
  7. Si desea proporcionar al agente contexto de la página, puede hacer clic en Extract para capturar todo el contenido visible de la página web.
  8. También puede añadir contexto adicional manualmente antes de hacer clic nuevamente en Send.

screenshoot of code assistant

Ahora que entendemos el flujo de trabajo, comencemos a implementar los archivos de la extensión.

manifest.json

El primer archivo que creamos es manifest.json, que configura los permisos de la extensión, la lógica de fondo y los scripts de contenido.

{
 "manifest_version": 3,
 "name": "Agent Sandbox",
 "version": "1.0",
 "description": "Chat with your Novita AI sandbox agent over WebSocket.",
 "permissions": [
   "contextMenus",
   "activeTab",
   "scripting"
 ],
 "host_permissions": [
   "ws://localhost:8000/*",
   "http://localhost:8000/*",
   "https://localhost:8000/*"
 ],
 "background": {
   "service_worker": "background.js"
 },
 "action": {
   "default_icon": "icon.png"
 },
 "content_scripts": [
   {
     "matches": ["<all_urls>"],
     "js": ["content.js"],
     "css": ["styles.css"]
   }
 ]
}

Este archivo le dice a Chrome qué scripts cargar, qué permisos necesitamos y qué archivo actúa como service worker.

background.js

El script de fondo es el service worker que se ejecuta en segundo plano. Es responsable de añadir nuestra extensión al menú contextual de Chrome y de escuchar las interacciones del usuario. Cuando el usuario selecciona nuestra opción de menú, el script de fondo envía un mensaje al script de contenido, que entonces activa el diálogo de la extensión.

chrome.runtime.onInstalled.addListener(() => {
 chrome.contextMenus.create({
   id: "ask-assistant",
   title: "Agent Sandbox",
   contexts: ["all"]
 });
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
 chrome.scripting.executeScript(
   {
     target: { tabId: tab.id },
     files: ["content.js"]
   },
   () => {
     chrome.tabs.sendMessage(tab.id, {
       type: "OPEN_PANEL"
     });
   }
 );
});

content.js

El script de contenido es responsable de mostrar el diálogo de la extensión dentro de la página web. Cuando recibe un mensaje del script de fondo, abre el diálogo. Este script es JavaScript simple. Gestiona la interfaz de usuario a través de operaciones DOM estándar y utiliza la API WebSocket para comunicarse con el servidor de la extensión.

let socket = null;

chrome.runtime.onMessage.addListener((msg) => {
 if (msg.type === "OPEN_PANEL") {
   openPanel();
 }
});

// -------------------------------------------------
// WEBPAGE TEXT EXTRACTOR
// -------------------------------------------------
function extractWebpageContent() {
 const cloned = document.cloneNode(true);
 cloned.querySelectorAll("script, style, iframe, noscript").forEach(e => e.remove());

 let main =
   cloned.querySelector("article") ||
   cloned.querySelector("main") ||
   cloned.querySelector("#content") ||
   cloned.body;

 const text = Array.from(main.querySelectorAll("h1, h2, h3, p"))
   .map(el => el.innerText.trim())
   .filter(Boolean)
   .join("\
\
");

 return text;
}

// -------------------------------------------------
// PANEL UI
// -------------------------------------------------
function openPanel() {
 const old = document.getElementById("assistant-box");
 if (old) old.remove();

 const box = document.createElement("div");
 box.id = "assistant-box";

 box.innerHTML = `
   <div id="assistant-container">
     <div id="assistant-header">
       <h3>Code Assistant</h3>
       <button id="assistant-close">×</button>
     </div>

     <textarea id="assistant-input" placeholder="Message Agent..."></textarea>

     <div class="btn-row">
       <button id="connect-btn">Connect</button>
       <button id="disconnect-btn">Disconnect</button>
       <button id="send-btn">Send</button>
       <button id="extract-btn">Extract</button>
     </div>

     <div id="assistant-result">Not connected.</div>
   </div>
 `;

 document.body.appendChild(box);

 document.getElementById("assistant-close").onclick = () => box.remove();

 const resultBox = document.getElementById("assistant-result");
 const inputBox = document.getElementById("assistant-input");

 // -------------------------------------------------
 // TEXT SELECTION LISTENER
 // -------------------------------------------------
 document.addEventListener("mouseup", () => {
   const selection = window.getSelection().toString().trim();
   if (!selection) return;

   // Append selected text to message box
   inputBox.value += (inputBox.value ? "\
\
" : "") + selection;

   // Scroll text area to bottom
   inputBox.scrollTop = inputBox.scrollHeight;
 });

 // -------------------------------------------------
 // CONNECT
 // -------------------------------------------------
 document.getElementById("connect-btn").onclick = () => {
   if (socket && socket.readyState === WebSocket.OPEN) {
     resultBox.innerText = "Already connected.";
     return;
   }

   socket = new WebSocket("ws://localhost:8000/ws");

   socket.onopen = () => {
     resultBox.innerText = "Connected to WebSocket.";
   };

   socket.onmessage = (event) => {
     const data = JSON.parse(event.data);
     resultBox.innerText += "\
\
Assistant:\
" + data.reply;
     resultBox.scrollTop = resultBox.scrollHeight;
   };

   socket.onerror = () => {
     resultBox.innerText = "WebSocket error.";
   };

   socket.onclose = () => {
     resultBox.innerText = "Disconnected.";
   };
 };

 // -------------------------------------------------
 // DISCONNECT
 // -------------------------------------------------
 document.getElementById("disconnect-btn").onclick = () => {
   if (socket) socket.close();
 };

 // -------------------------------------------------
 // SEND MESSAGE
 // -------------------------------------------------
 document.getElementById("send-btn").onclick = () => {
   const context = inputBox.value;

   if (!socket || socket.readyState !== WebSocket.OPEN) {
     resultBox.innerText = "Not connected.";
     return;
   }

   socket.send(JSON.stringify({ message: context }));

   inputBox.value = "";

   resultBox.innerText += "\
\
You:\
" + context;
 };

 // -------------------------------------------------
 // EXTRACT PAGE → ADD TO MESSAGE BOX
 // -------------------------------------------------
 document.getElementById("extract-btn").onclick = () => {
   const extracted = extractWebpageContent();

   if (!extracted || extracted.length < 10) {
     resultBox.innerText = "Could not extract useful content.";
     return;
   }

   // Add extracted text to input box (not sent automatically)
   inputBox.value += (inputBox.value ? "\
\
" : "") + extracted;

   // Scroll text area
   inputBox.scrollTop = inputBox.scrollHeight;

   resultBox.innerText = "📄 Extracted content added to message box.";
 };
}

styles.css

Luego usamos el archivo styles.css para dar estilo a la interfaz y controlar cómo aparece la extensión en la página.

#assistant-box {

 position: fixed;

 top: 10%;

 right: 10%;

 width: 350px;

 background: #000;

 border: 1px solid #00ff7f;

 border-radius: 8px;

 box-shadow: 0 0 10px rgba(0,255,127,0.5);

 z-index: 999999;

 font-family: monospace;

 color: #00ff7f;

}

#assistant-container {

 padding: 12px;

}

#assistant-header {

 display: flex;

 justify-content: space-between;

 align-items: center;

}

#assistant-close {

 background: transparent;

 border: none;

 font-size: 20px;

 cursor: pointer;

 padding: 0 5px;

 font-weight: bold;

 color: #00ff7f;

}

#assistant-container textarea {

 width: 100%;

 height: 60px;

 margin-top: 8px;

 background: #0d0d0d;

 color: #00ff7f;

 border: 1px solid #00ff7f;

 border-radius: 4px;

 padding: 6px;

 resize: vertical;

 outline: none;

}

#send-btn {

 width: 100%;

 margin-top: 10px;

 background: #00ff7f;

 border: none;

 padding: 10px;

 color: #000;

 font-weight: bold;

 cursor: pointer;

 border-radius: 4px;

 transition: 0.2s;

}

#send-btn:hover {

 background: #00e66a;

}

#assistant-box pre {

 background: #0a0a0a;

 padding: 8px;

 border-radius: 4px;

 max-height: 120px;

 overflow: auto;

 white-space: pre-wrap;

 word-break: break-word;

 margin-top: 5px;

}

#assistant-result {

 margin-top: 12px;

 background: #0d0d0d;

 padding: 8px;

 border-radius: 5px;

 white-space: pre-wrap;

 max-height: 150px;

 overflow: auto;

}

Cómo configurar la extensión de Chrome

Ahora que todos los archivos necesarios están en su lugar, el siguiente paso es cargar la extensión en Chrome. Sigue estos pasos:

  1. Guarda el código del servidor de la extensión en un archivo Python.
  2. Instala todas las dependencias y configura las variables de entorno necesarias para el servidor.
  3. Crea una nueva carpeta en tu máquina llamada code-assistant-extension para el cliente de la extensión.
  4. Agrega los siguientes archivos dentro de la carpeta:
    • manifest.json
    • background.js
    • content.js
    • styles.css
  5. Abre Chrome y ve a: chrome://extensions/
  6. Activa el Modo desarrollador en la esquina superior derecha.
  7. Haz clic en Cargar extensión sin empaquetar.
  8. Selecciona la carpeta que contiene los archivos de tu extensión.
  9. La extensión aparecerá en la barra de herramientas.
  10. Haz clic derecho en cualquier selección de texto en una página web para ver Agent Sandbox en el menú contextual.

Tu extensión está ahora lista para usar.

Repositorio: AI Code Assistant Browser Extension

Conclusión

En este artículo construimos una extensión de Chrome que se conecta a un backend impulsado por Novita Sandbox, permitiendo que un agente de IA ejecute código de forma segura y asista a los usuarios mientras navegan. Este patrón va más allá de la ayuda para codificar; puede potenciar herramientas de aprendizaje interactivas, asistentes de depuración, mejoradores de documentación y más.

La arquitectura es independiente del navegador, lo que significa que el mismo enfoque puede adaptarse a cualquier navegador moderno con cambios mínimos. A partir de aquí puedes ampliar las capacidades del asistente, refinar la interfaz de usuario o agregar nuevas herramientas para el sandbox. Esta base abre la puerta a la creación de compañeros de navegación inteligentes y poderosos.

Novita AI es una plataforma líder en la nube de IA que proporciona a los desarrolladores APIs fáciles de usar e infraestructura GPU asequible y confiable para construir y escalar aplicaciones de IA.