Представьте, что вы просматриваете сайты с документацией или учебники по программированию и никогда не чувствуете себя одиноким. Вместо того чтобы разбираться со всем самостоятельно, у вас есть ИИ-ассистент, который следует за вами с страницы на страницу. Он не привязан к какому-то одному веб-сайту. Он всегда рядом с вами, готовый помочь. Он может безопасно выполнять код, который вы встречаете в интернете, давать объяснения и предоставлять полезные сведения сразу, как только вы в них нуждаетесь.
Итак, как реализовать такой опыт? Создав расширение для браузера. Это расширение будет включать ИИ-агента, с которым пользователь может общаться, и у агента будет доступ к защищенной песочнице, где он может безопасно запускать код и выполнять другие операции.
Для создания этой системы мы разработаем расширение для Chrome, используем модель Novita, поддерживающую работу с инструментами, и интегрируем Novita Sandbox в качестве безопасной среды выполнения для агента. В этой статье мы подробно разберем весь процесс его создания.

К концу этого руководства вы узнаете:
- Как создать расширение для Chrome, интегрированное с ИИ-агентом
- Как использовать агентные LLM от Novita
- Как настроить Novita Sandbox в качестве безопасной среды для агента браузера
- Как заставить расширение общаться с агентом в реальном времени
Песочница: единственный инструмент, который вам нужен
Расширение для Chrome, которое мы разрабатываем, опирается на ИИ-агента для помощи пользователю. Поскольку это расширение предназначено для работы в качестве кодового ассистента, агенту нужна возможность запускать код, создавать файлы, проверять вывод и выполнять все типичные задачи, с которыми может столкнуться разработчик. Вы можете ожидать, что для этого потребуется длинный список инструментов, но на самом деле нужна только одна вещь: песочница.
Песочница предоставляет агенту доступ к среде Linux, где он может запускать команды, создавать и изменять файлы, а также выполнять любые операции, которые вы обычно выполняете в терминале. Для этого проекта мы будем использовать Novita Sandbox.
Для настройки сначала установите пакет Novita Sandbox:
pip install novita-sandbox
Далее установите переменную окружения NOVITA_API_KEY равной вашему API-ключу. После того как это будет сделано, вы можете создать и использовать песочницу следующим образом:
from novita_sandbox.code_interpreter import Sandbox
sandbox = Sandbox.create()
result = sandbox.commands.run('ls -l')
print(result)
sandbox.kill()
Этот фрагмент кода создает песочницу, запускает команду ls -l, выводит результат и затем завершает работу песочницы. Этот простой рабочий процесс является основой того, как наш браузерный ассистент будет использовать песочницу для помощи пользователям.
Теперь давайте применим этот принцип к полноценному расширению.
Архитектура агента браузерного ассистента
Архитектура этого проекта построена по модели клиент-сервер. Расширение для Chrome выступает в роли клиента, а выделенный бэкенд-сервер размещает как ИИ-агента, так и среду песочницы.
Расширение взаимодействует с сервером через соединение WebSocket. Это позволяет обмениваться сообщениями в реальном времени в обоих направлениях, поэтому запросы пользователя и ответы агента передаются мгновенно, без заметной задержки. Сервер, в свою очередь, взаимодействует с API Novita, которые включают endpoint модели и сервис песочницы.
Вместе расширение и бэкенд образуют интеллектуального браузерного ассистента, способного безопасно запускать код, быстро обрабатывать информацию и предоставлять полезные объяснения прямо в процессе просмотра пользователем веб-страниц.
Разработка расширения
Теперь, когда мы понимаем общую архитектуру, мы можем начать реализацию самого расширения. Мы начнем с сервера плагина.
Сервер расширения
Сервер расширения — это простой WebSocket-сервис с единственным endpoint /ws. Этот endpoint получает сообщения от пользователя и возвращает ответы LLM в реальном времени. Он также обрабатывает вызовы инструментов, вызывая песочницу каждый раз, когда агенту нужно выполнить код или провести операцию.
Зависимости
Сервер опирается на три основные библиотеки:
- FastAPI: HTTP-фреймворк, который предоставляет реализацию WebSocket
- OpenAI: SDK, используемый для взаимодействия с моделями Novita
- Novita Sandbox: безопасная среда, в которой код выполняется изолированно
Установите их с помощью:
pip install novita-sandbox "fastapi[standard]" openai
Установите ваш API-ключ Novita в качестве переменной окружения:
export NOVITA_API_KEY = sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Написание кода сервера
Начните с импорта необходимых модулей:
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
Инициализация клиента LLM
Далее создайте клиент OpenAI, который указывает на API Novita. В этом примере мы используем модель llama-3.3-70b-instruct, но подойдет любая модель Novita, которая поддерживает вызов инструментов.
client = OpenAI(
base_url="https://api.novita.ai/openai",
api_key=os.environ["NOVITA_API_KEY"],
)
model = "meta-llama/llama-3.3-70b-instruct"
Определение схемы инструментов
Агент будет использовать четыре инструмента, каждый из которых взаимодействует с песочницей:
- read_file: Читает содержимое файла
- write_file: Создает и записывает данные в один файл
- write_files: Создает и записывает данные в несколько файлов
- run_commands: Выполняет команды оболочки внутри песочницы
Полная схема инструментов приведена ниже:
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"],
},
},
},
]
Настройка HTTP-сервера
Настройте FastAPI и включите CORS, чтобы расширение для Chrome могло отправлять запросы на сервер.
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
Реализация инструментов
Определите функцию-обработчик, которая реализует каждый инструмент и направляет вызовы инструментов агента в песочницу:
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,
}
Создание WebSocket-endpoint
Теперь реализуем WebSocket-endpoint. Когда пользователь подключается к endpoint, создается новый экземпляр песочницы.
Вся коммуникация между пользователем и агентом проходит через это соединение. Если пользователь просит агента использовать инструмент, агент выбирает и выполняет соответствующий инструмент через функцию-обработчик.
Когда соединение закрывается, работа песочницы завершается.
@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")
Запуск сервера
Наконец, используйте Uvicorn для запуска сервиса:
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Это завершает компонент сервера.
Расширение
Расширение — это интерфейс, с которым взаимодействует пользователь. Оно состоит из небольшого набора файлов, которые работают вместе для запуска на любой веб-странице. После активации пользователь может общаться с сервером расширения в реальном времени прямо со страницы, которую он просматривает.
Расширение включает следующие файлы:
- manifest.json: Определяет конфигурацию расширения и разрешения
- background.js: Содержит логику сервис-воркера и обрабатывает действия контекстного меню
- content.js: Управляет взаимодействиями на странице и отображает диалоговое окно ассистента
- styles.css: Определяет стили для окна ассистента на странице
У каждого файла есть отдельная ответственность, и вместе они образуют полноценное рабочее расширение.
Как работает расширение
Прежде чем мы реализуем файлы, давайте кратко посмотрим, как расширение работает с точки зрения пользователя:

- Пользователь кликает правой кнопкой мыши на любой веб-странице, и появляется контекстное меню.
- Он выбирает пункт Agent Sandbox в меню.
- Расширение открывает диалоговое окно ассистента на странице.
- Пользователь нажимает кнопку Connect для установки соединения с сервером расширения.
- После установки соединения пользователь может начать вводить текст прямо в поле для сообщений.
- После ввода сообщения он просто нажимает кнопку Send для отправки его на сервер.
- Если он хочет предоставить агенту контекст страницы, он может нажать кнопку Extract, чтобы захватить все видимое содержимое веб-страницы.
- Он также может добавить дополнительный контекст вручную перед повторным нажатием кнопки Send.

Теперь, когда мы понимаем рабочий процесс, давайте начнем реализацию файлов расширения.
manifest.json
Первый файл, который мы создаем — manifest.json, он определяет конфигурацию расширения, разрешения, фоновую логику и контентные скрипты.
{
"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"]
}
]
}
Этот файл сообщает Chrome, какие скрипты загружать, какие разрешения нужны и какой файл выступает в роли сервис-воркера.
background.js
Фоновый скрипт — это сервис-воркер, который работает в фоновом режиме. Он отвечает за добавление нашего расширения в контекстное меню Chrome и прослушивание взаимодействий пользователя. Когда пользователь выбирает наш пункт меню, фоновый скрипт отправляет сообщение контентному скрипту, который затем активирует диалоговое окно расширения.
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
Контентный скрипт отвечает за отображение диалогового окна расширения внутри веб-страницы. Когда он получает сообщение от фонового скрипта, он открывает диалоговое окно. Этот скрипт написан на обычном JavaScript. Он управляет интерфейсом через стандартные операции с DOM и использует API WebSocket для взаимодействия с сервером расширения.
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
Затем мы используем файл styles.css для стилизации интерфейса и управления внешним видом расширения на странице.
#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;
}
Как настроить расширение для Chrome
Теперь, когда все необходимые файлы готовы, следующий шаг — загрузить расширение в Chrome. Выполните следующие действия:
- Сохраните код сервера расширения в Python-файл.
- Установите все зависимости и настройте необходимые переменные окружения для сервера.
- Создайте на вашем компьютере новую папку с именем code-assistant-extension для клиентской части расширения.
- Добавьте в папку следующие файлы:
- manifest.json
- background.js
- content.js
- styles.css
- Откройте Chrome и перейдите по адресу:
chrome://extensions/ - Включите Режим разработчика в правом верхнем углу.
- Нажмите кнопку Загрузить распакованное расширение.
- Выберите папку, содержащую файлы вашего расширения.
- Расширение появится на панели инструментов.
- Кликните правой кнопкой мыши на любом выделенном тексте на веб-странице, чтобы в контекстном меню появился пункт Agent Sandbox.
Ваше расширение теперь готово к использованию.
Репозиторий — AI Code Assistant Browser Extension
Заключение
В этой статье мы разработали расширение для Chrome, которое подключается к бэкенду на базе Novita Sandbox, позволяя ИИ-агенту безопасно выполнять код и помогать пользователям во время просмотра веб-страниц. Этот паттерн выходит за рамки помощи в программировании; на его основе можно создавать интерактивные обучающие инструменты, ассистенты для отладки, улучшители документации и многое другое.
Архитектура не привязана к конкретному браузеру, значит, тот же подход можно адаптировать под любой современный браузер с минимальными изменениями. Отсюда вы можете расширять возможности ассистента, улучшать интерфейс или добавлять новые инструменты для песочницы. Эта основа открывает возможности для создания мощных, интеллектуальных браузерных компаньонов.
Novita AI— ведущая облачная ИИ-платформа, которая предоставляет разработчикам простые в использовании API и доступную надежную GPU-инфраструктуру для создания и масштабирования ИИ-приложений
