Создание кодирующего агента с помощью Novita Agent Sandbox

Создание кодирующего агента с помощью Novita Agent Sandbox

У вас есть ИИ-агент, и вы хотите запустить его в защищенной среде, где у него будет доступ к необходимым ресурсам, таким как файловая система и возможность выполнять команды (например, команды оболочки), без риска «сломать что-либо». Какие у вас есть варианты?

Лучший подход — предоставить вашему ИИ-агенту песочницу. Внутри песочницы агент может безопасно взаимодействовать с машиной Linux, работать с файловой системой и запускать определенные команды, при этом ему запрещено выполнять потенциально опасные операции.

С такой настройкой мы можем создавать мощные приложения. Например, кодирующий агент, который может:

  • Создавать и редактировать файлы с кодом в файловой системе
  • Выполнять команды вроде git, python или node
  • Сотрудничать с разработчиками, выполняя и тестируя код прямо в среде

В этой статье мы шаг за шагом создадим такой кодирующий агент. Мы будем использовать LLM от Novita с поддержкой вызова функций в паре с песочницей Novita Agent Sandbox в качестве защищенной среды. В довершение мы создадим удобный интерфейс с помощью Gradio и развернем его на Hugging Face Spaces.

Приступим!

Песочница для агентов

Novita’s Agent Sandbox — это среда выполнения, разработанная специально для ИИ-агентов. Она предоставляет защищенную и изолированную облачную конфигурацию, которая работает как виртуальный компьютер. Внутри этой среды агенты могут безопасно выполнять сгенерированный код, не рискуя основной системой.

Ключевые особенности песочницы Novita Agent Sandbox

  • Защищенность: Песочница полностью изолирована, поэтому агент имеет доступ только к собственным ресурсам.
  • Быстрый запуск: Новые среды поднимаются менее чем за 200 мс.
  • Виртуальная машина: Поскольку песочница работает как ВМ, агенты могут запускать код на любом языке программирования.
  • Пауза и возобновление: Вы можете приостановить работу песочницы в любой момент и возобновить ее позже.
  • Фоновые задачи: Агенты могут запускать задачи в фоновом режиме и асинхронно получать результаты.

Установка SDK

Для использования песочницы Novita вам понадобится SDK, который поддерживает как Python, так и TypeScript/JavaScript. В этом руководстве мы будем использовать Python SDK:

pip install novita-sandbox

После установки задайте ваш API-ключ Novita в качестве переменной окружения:

export NOVITA\_API\_KEY=your\_api\_key\_here

Тестирование песочницы

После настройки всего необходимого давайте создадим песочницу и выполним несколько базовых операций:

from novita_sandbox.code_interpreter import Sandbox

sandbox = Sandbox.create()

files = sandbox.files.list("/")

for file in files:
    print(file.name)

result = sandbox.commands.run('pwd')
print(result)

sandbox.kill()

В этом примере показано, как:

  • Создать экземпляр песочницы
  • Получить доступ к файловой системе через объект files
  • Выполнять команды с помощью метода commands.run
  • Освобождать ресурсы с помощью kill после завершения работы

Теперь, когда мы изучили основы работы с файловой системой и выполнения команд, мы готовы создать наш кодирующий агент, который будет использовать эти возможности песочницы в качестве инструментов.

Создание кодирующего агента

Для создания кодирующего агента нам нужна LLM, которая поддерживает вызов функций. Novita предоставляет несколько моделей, которые могут это делать. Чтобы наш агент вел себя как помощник по программированию, у него должен быть правильный набор функций.

Давайте подумаем, что обычно делает программист. Он пишет, читает и выполняет код. Поэтому наш агент должен уметь:

  • Записывать данные в файл
  • Читать данные из файла
  • Выполнять команды
  • Записывать данные в несколько файлов одновременно

Настройка агента

Поскольку модели Novita совместимы с OpenAI, мы можем использовать OpenAI SDK для взаимодействия с ними. Давайте установим его:

pip install openai

После установки задайте ваш API-ключ Novita в качестве переменной окружения, как мы делали это раньше. После этого мы можем начать писать код, добавив импорты:

from openai import OpenAI
import os
import json
from novita_sandbox.code_interpreter import Sandbox

Теперь давайте создадим экземпляр клиента OpenAI:

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

Здесь мы указываем клиенту базовый URL Novita вместо URL OpenAI и используем наш API-ключ Novita для аутентификации.

Далее мы создадим экземпляр песочницы, который будет использовать наш агент:

sandbox = Sandbox.create(timeout=1200)

Параметр timeout указывает, сколько времени песочница должна оставаться активной. В этом случае мы задали значение 10 минут.

Определение функций

Теперь мы можем определить функции, которые будет использовать наш агент.

1. Чтение файла

Эта функция принимает путь к файлу и читает его содержимое с помощью объекта files песочницы.

def read_file(path: str):

   print(f"[DEBUG] read_file called with path: {path}")

   try:

       content = sandbox.files.read(path)

       print(f"[DEBUG] read_file result: {content}")

       return content  # returns string content

   except Exception as e:

       print(f"[DEBUG] read_file error: {e}")

       return f"Error reading file: {e}"

2. Запись в файл

Эта функция записывает данные по указанному пути к файлу.

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

3. Запись нескольких файлов

Эта функция работает так же, как write_file, но обрабатывает несколько файлов одновременно.

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

4. Выполнение команд

Эта функция выполняет команды оболочки внутри песочницы и возвращает стандартный вывод.

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

Регистрация инструментов

Теперь, когда у нас есть все функции, мы зарегистрируем их в качестве инструментов, которые LLM может вызывать по необходимости. Каждое определение инструмента включает имя функции, описание и схему параметров.

tools = [

   {

       "type": "function",

       "function": {

           "name": "read_file",

           "description": "Read contents of a file inside the sandbox",

           "parameters": {

               "type": "object",

               "properties": {

                   "path": {"type": "string", "description": "File path in the sandbox"}

               },

               "required": ["path"],

           },

       },

   },

   {

       "type": "function",

       "function": {

           "name": "write_file",

           "description": "Write a single file inside the sandbox",

           "parameters": {

               "type": "object",

               "properties": {

                   "path": {"type": "string", "description": "File path in the sandbox"},

                   "data": {"type": "string", "description": "Content to write"},

               },

               "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 single shell command inside the sandbox working directory",

           "parameters": {

               "type": "object",

               "properties": {

                   "command": {

                       "type": "string",

                       "description": "The shell command to run, e.g. 'ls' or 'python main.py'",

                   }

               },

               "required": ["command"],

           },

       },

   }

]

После регистрации инструментов перейдем к созданию цикла чата, который будет использовать нашего агента и все определенные нами инструменты.

Цикл чата

Теперь мы создадим простой цикл чата, который позволит пользователю взаимодействовать с кодирующим агентом. Цикл будет хранить список сообщений и обрабатывать вызовы функций всякий раз, когда агент запрашивает их.

messages = []

print("💬 Enter your queries (type 'exit' to quit):")

while True:

   user_input = input("You: ")

   if user_input.lower() == "exit":

       break

   # Append user message

   messages.append({"role": "user", "content": user_input})

   # Send to model

   response = client.chat.completions.create(

       model=model,

       messages=messages,

       tools=tools,

   )

   assistant_msg = response.choices[0].message

   messages.append(assistant_msg)

   if assistant_msg.tool_calls:

       print(f"[DEBUG] Assistant requested {len(assistant_msg.tool_calls)} tool call(s).")

       for tool_call in assistant_msg.tool_calls:

           fn_name = tool_call.function.name

           fn_args = json.loads(tool_call.function.arguments)

           print(f"[DEBUG] Tool call detected: {fn_name} with args {fn_args}")

           if fn_name == "read_file":

               fn_result = read_file(**fn_args)

           elif fn_name == "write_file":

               fn_result = write_file(**fn_args)

           elif fn_name == "write_files":

               fn_result = write_files(**fn_args)

           elif fn_name == "run_commands":

               fn_result = run_commands(**fn_args)

           else:

               fn_result = f"Error: Unknown tool {fn_name}"

               print(f"[DEBUG] Unknown tool requested: {fn_name}")

           # Append result back

           messages.append({

               "tool_call_id": tool_call.id,

               "role": "tool",

               "content": str(fn_result),

           })

       # Get model's final answer with tool results

       follow_up = client.chat.completions.create(

           model=model,

           messages=messages,

       )

       final_answer = follow_up.choices[0].message

       messages.append(final_answer)

       print("Assistant:", final_answer.content)

   else:

       print("Assistant:", assistant_msg.content)

sandbox.kill()

print("[DEBUG] Sandbox terminated. 👋")

Этот цикл чата поддерживает взаимодействие, позволяет агенту вызывать любой из зарегистрированных инструментов по необходимости и очищает песочницу при выходе пользователя.

Создание интерфейса с помощью Gradio

Теперь у нас есть полностью функциональный кодирующий агент, который может общаться с нами, но взаимодействие через REPL не очень увлекательно. Давайте сделаем опыт более интересным, предоставив нашему агенту простой интерфейс Gradio.

Создание интерфейса Gradio простое. Мы будем использовать gr.ChatInterface для управления взаимодействием в чате и свяжем его с логикой, которую мы создали ранее. Кроме того, мы добавим интерфейс команд для выполнения команд оболочки внутри песочницы, а также выпадающее меню для выбора модели.

Чтобы обновить наш предыдущий код для поддержки Gradio, мы заменим цикл чата и последние две строки на следующий:

    # --- Persistent chat messages ---

    messages = []

    # --- Global model setter ---

    def set_model(selected_model):

       global model

       model = selected_model

       print(f"[DEBUG] Model switched to: {model}")

       return f"✅ Model switched to **{model}**"

    def chat_fn(user_message, history):

       global messages, model

       messages.append({"role": "user", "content": user_message})

       # Send to model

       response = client.chat.completions.create(

           model=model,

           messages=messages,

           tools=tools,

       )

       assistant_msg = response.choices[0].message

       messages.append(assistant_msg)

       output_text = ""

       if assistant_msg.tool_calls:

           print(f"[DEBUG] Assistant requested {len(assistant_msg.tool_calls)} tool call(s).")

           for tool_call in assistant_msg.tool_calls:

               fn_name = tool_call.function.name

               fn_args = json.loads(tool_call.function.arguments)

               print(f"[DEBUG] Tool call detected: {fn_name} with args {fn_args}")

               if fn_name == "read_file":

                   fn_result = read_file(**fn_args)

               elif fn_name == "write_file":

                   fn_result = write_file(**fn_args)

               elif fn_name == "write_files":

                   fn_result = write_files(**fn_args)

               elif fn_name == "run_commands":

                   fn_result = run_commands(**fn_args)

               else:

                   fn_result = f"Error: Unknown tool {fn_name}"

               messages.append({

                   "tool_call_id": tool_call.id,

                   "role": "tool",

                   "content": str(fn_result),

               })

           follow_up = client.chat.completions.create(

               model=model,

               messages=messages,

           )

           final_answer = follow_up.choices[0].message

           messages.append(final_answer)

           output_text = final_answer.content

       else:

           output_text = assistant_msg.content

       return output_text

    # --- Command Interface function ---

    def execute_command(command):

       if not command.strip():

           return "⚠️ Please enter a command."

       print(f"[DEBUG] Executing command from interface: {command}")

       output = run_commands(command)

       return f"```bash\
    {output}\
    ```" if output else "✅ Command executed (no output)."

    # --- Gradio UI ---

    with gr.Blocks(title="Novita Sandbox App") as demo:

       gr.Markdown("## 🧠 Novita Sandbox Agent")

       gr.Markdown(

       "This app is an AI-powered **code agent** that lets you chat with intelligent assistants backed by **Novita AI LLMs**. These agents can write, read, and execute code safely inside a **Novita sandbox**, providing a secure environment for running commands, testing scripts, and managing files, all through an intuitive chat interface with model selection and command execution built right in."

    )

       with gr.Row(equal_height=True):

           # Left: Chat Interface

           with gr.Column(scale=2):

               gr.Markdown("### 💬 Chat Interface")

               gr.ChatInterface(chat_fn)

           # Right: Command Interface

           with gr.Column(scale=1):

               gr.Markdown("### 💻 Command Interface")

               # Model selector

               model_selector = gr.Dropdown(

                   label="Select Model",

                   choices=[

                       "meta-llama/llama-3.3-70b-instruct",

                       "deepseek/deepseek-v3.2-exp",

                       "qwen/qwen3-coder-30b-a3b-instruct",

                       "openai/gpt-oss-120b",

                       "moonshotai/kimi-k2-instruct",

                   ],

                   value=model,

                   interactive=True,

               )

               model_status = gr.Markdown(f"✅ Current model: **{model}**")

               model_selector.change(set_model, inputs=model_selector, outputs=model_status)

               command_input = gr.Textbox(

                   label="Command",

                   placeholder="e.g., ls, python main.py",

                   lines=1,

               )

               with gr.Row():

                   run_btn = gr.Button("Run", variant="primary", scale=0)

               command_output = gr.Markdown("Command output will appear here...")

               run_btn.click(execute_command, inputs=command_input, outputs=command_output)

    # --- Cleanup on exit ---

    atexit.register(lambda: (sandbox.kill(), print("[DEBUG] Sandbox terminated. 👋")))

    if __name__ == "__main__":

       demo.launch()

В этой версии функция chat_fn обрабатывает каждый обмен сообщениями между пользователем и агентом. gr.ChatInterface принимает эту функцию как входные данные и автоматически управляет взаимодействием с интерфейсом.

При запуске приложения Gradio ваш агент работает в браузере, предоставляя пользователю чистый интерактивный чат-интерфейс. Наконец, мы регистрируем процедуру очистки с помощью atexit, чтобы убедиться, что песочница корректно завершает работу при остановке приложения.

Теперь у нас есть кодирующий агент на базе ИИ, который безопасно работает в песочнице и оснащен удобным чат-интерфейсом Gradio.

Полный код вы можете найти на GitHub.

Тестирование кодирующего агента

Чтобы использовать агента, нам нужно запустить наш код Gradio как скрипт:

python gradio_chat.py

После этого наше приложение Gradio будет работать на localhost. Так мы сможем общаться с нашим кодирующим агентом, и агент будет выполнять все наши действия внутри песочницы.

В приложении Gradio мы видим только чаты с агентом, но если мы перейдем в терминал, мы также увидим отладочные выводы: какие команды вызвала функция, чтобы помочь запросу пользователя.

Тот факт, что у нас есть доступ к инструменту работы с файлами и инструменту выполнения команд, означает, что мы могут написать практически любой код. Но вместо того, чтобы писать код напрямую, мы даем инструкции нашему агенту, и он сам пишет и выполняет код за нас.

Развертывание на Hugging Face Spaces

В настоящее время наш кодирующий агент работает локально на нашем компьютере. Теперь давайте сделаем его доступным для всего мира, развернув на Hugging Face Spaces. Hugging Face Spaces позволяет разместить и наш код, и приложение в одном месте. Приступим.

Создание пространства

Перейдите на Hugging Face и создайте новое пространство для вашего кодирующего агента, задав ему уникальное имя.

Далее выберите SDK для пространства: в нашем случае это Gradio. Выберите пустой шаблон, так как у нас уже есть код приложения.

Затем выберите оборудование. Поскольку наш агент и песочница работают на Novita, нам не нужно специализированное оборудование. Достаточно опции базового CPU. После этого нажмите «Создать пространство».

Hugging Face создаст пространство с файлами README.md и .gitignore.

Есть несколько способов добавить ваш код, но самый простой — нажать «Contribute» → «Add file».

Создайте файл requirements.txt и добавьте в него следующие зависимости:

openai
novita-sandbox

Добавление переменных окружения

Перед запуском приложения нам нужно задать NOVITA_API_KEY в качестве переменной окружения.

Для этого перейдите в настройки вашего пространства, прокрутите до раздела «Variables and secrets» и добавьте новый секрет с именем NOVITA_API_KEY, указав в качестве значения ваш API-ключ.

Настройка приложения

После задания переменной окружения пришло время создать наше приложение.

Создайте новый файл с именем app.py и вставьте в него полный код агента Gradio.

После сохранения файла Hugging Face автоматически начнет сборку вашего пространства.

После завершения процесса сборки ваш кодирующий агент будет запущен и доступен на Hugging Face Spaces.

Теперь вы можете общаться с агентом через интерактивный чат-интерфейс.

Кроме того, вы можете отслеживать логи, чтобы видеть, какие инструменты вызывает агент во время выполнения.

И это все! Теперь у вас есть полностью функциональный кодирующий агент, работающий в защищенной песочнице, оснащенный интерфейсом Gradio и бесшовно развернутый на Hugging Face Spaces.

Заключение

В этой статье мы рассмотрели, как использовать песочницу Novita для создания полностью функционального кодирующего агента, способного читать и создавать файлы, выполнять команды и безопасно работать в защищенной среде.

То, что мы создали здесь, — это только начало. Песочница открывает двери бесчисленным возможностям: от создания агентов для визуализации данных на базе ИИ до разработки агентов для работы с компьютерами, которые могут интеллектуально взаимодействовать с системами.

Практически все возможно, когда вы сочетаете агента с специализированным набором инструментов, таким как песочница.