Головна / Статті / Практичні поради: Оновіть ваш Deep Agent за допомогою локальної пісочниці з відкритим кодом

Практичні поради: Оновіть ваш Deep Agent за допомогою локальної пісочниці з відкритим кодом

Покрокова інструкція до практичних порад: як оновити ваш Deep Agent за допомогою локального сандбоксу з відкритим кодом: контракти, перевірки та слоти для вставки коду для команд, які використовують цю схему.

3190 слів

Використовуйте цей документ як оновлену версію ідей з матеріалу «Покращте свого Deep Agent за допомогою локального сандбоксу з відкритим кодом» для операторів: чіткі етапи, впорядковані блоки коду та примітки щодо відновлення, які зберігаються після передачі обов’язків. Етап Огляду найкраще функціонує, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітки щодо скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевірити, не читаючи весь кодовий граф.

Що таке сандбокс та чому агентам він потрібен

На етапі «Що таке пісочниця» необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори мають мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Необхідно документувати як шлях успішного виконання, так і шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Встановлюйте людське схвалення для тих кроків, які призводять до витрат грошей або змінюють дані у продакшені. Підключення на етапі компіляції не є гарантією повності функціоналу продукту.

Проблема

На етапі реалізації необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Краще використовувати невеликі, перевірювані одиниці коду замість об’ємних скриптів. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану структуру процесу. Необхідно встановити людське схвалення для операцій, які призводять до витрат грошей чи змінюють дані у продакшені. Підключення елементів під час компіляції не є гарантією повноти бізнес-функціоналу.

Увійдіть до OpenSandbox

Для етапу Enter OpenSandbox необхідно визначити вхідні дані, власника кроку та критерії завершення ще до зміни коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Призначте назви артефактів, визначте критерії успіху та не допускайте мовчазного часткового завершення. Забезпечте людське схвалення для операцій, які витрачають гроші чи змінюють дані в продакшені. Підключення під час компіляції не є гарантією повноти бізнес-процесу. Для етапу Enter OpenSandbox необхідно визначити вхідні дані, власника кроку та критерії завершення ще до зміни коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Зберігайте конфігурацію окремо від коду додатку. Файли середовища, сховища секретних даних та флаги функцій мають знаходитися в одному місці, яке оператори можуть перевірити, не читаючи весь кодовий граф.

Точка розширення сандбоксу Deep Agents

Під час роботи над етапом розширення сандбоксу Deep Agents спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допоможе зберегти чесність подальших змін у коді. Одночасно задокументуйте шлях успішного виконання та шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої доробки. Створюйте контрольні точки після дорогих операцій. Функція відновлення не повинна знову оплачувати один і той самий виклик LLM, коли оператор намагається знову виконати пізнішу операцію.

class BaseSandbox(ABC):
    def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse: ...
    @property
    def id(self) -> str: ...
    def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]: ...
    def download_files(self, paths: list[str]) -> list[FileDownloadResponse]: ...

Створення інтеграції

Під час виконання етапу створення інтеграції спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Якщо якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність операцій. Робіть перевірки після дорогих кроків. Система повторного запуску не повинна знову стягувати плату за один і той самий виклик LLM, коли оператор намагається виконати наступний етап.

Як працює OpenSandbox?

Під час роботи над етапом «Як працює OpenSandbox» спочатку запишіть угоду: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність у подальших змінах коду. Розглядайте цей етап як угоду між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте беззвучного часткового виконання завдань. Робіть контрольні пункти після дорогих кроків. Система відновлення не повинна знову оплачувати один і той самий виклик LLM, коли оператор перезапускає пізнішу ланку. Під час роботи над етапом «Як працює OpenSandbox» спочатку запишіть угоду: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність у подальших змінах коду. Зберігайте конфігурацію окремо від коду програми. Файли середовища, сховища секретів та флаги функцій мають знаходитися в одному місці, яке оператори можуть перевіряти, не читаючи весь код.

# Generate a starter config
uvx opensandbox-server init-config ~/.sandbox.toml --example docker

# Start the server
uvx opensandbox-server

Спрощений код інтеграції

Етап коду інтеграції у спрощеній версії працює найкраще, якщо його розглядати як вимірювану поверхню. Зафіксуйте один ідеальний варіант роботи, один випадок збою та примітку щодо скасування змін перед розширенням обсягу. Документуйте як успішний, так і відновлювальний сценарії роботи разом. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Зберігайте стан графа у простій та типованій формі. Вкладені структури приховують інформацію про те, який вузол заповнив яке поле, і ускладнюють продовження роботи після перерв.

.create()

Етап створення працює найкраще, якщо його розглядати як вимірювану поверхню. Запишіть один ідеальний приклад роботи, один випадок збою та примітку щодо скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям перед об’ємними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на складну послідовність дій. Зберігайте стан графа у простій та типованій формі. Вкладені структури приховують інформацію про те, який вузол заповнив яке поле, і ускладнюють продовження роботи після перерв.

class MinimalOpenSandboxBackend(BaseSandbox):
    def __init__(self, sandbox: Sandbox, runner: AsyncRunner):
        self._sandbox = sandbox
        self._runner = runner
from opensandbox import Sandbox
from opensandbox.config import ConnectionConfig

IMAGE = "opensandbox/code-interpreter:v1.1.0"
ENTRYPOINT = ["/opt/code-interpreter/code-interpreter.sh"]

@classmethod
def create(cls, api_key: str | None = None) -> "MinimalOpenSandboxBackend":
    runner = AsyncRunner()
    config = ConnectionConfig(domain="localhost:8080", api_key=api_key)
    sandbox = runner.run(
        Sandbox.create(IMAGE, entrypoint=ENTRYPOINT, connection_config=config, timeout=timedelta(minutes=30))
    )
    return cls(sandbox, runner)

.id()

Цей етап працює найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте мовчазного часткового виконання завдань. Зберігайте стан графа у випрямленому та типованому форматі. Вкладені структури приховують інформацію про те, який вузол заповнив яке поле, що ускладнює продовження роботи після перерв. Цей етап працює найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Зберігайте конфігурацію окремо від коду програми. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь граф.

@property
def id(self) -> str:
    return self._sandbox.id

.execute()

На етапі виконання необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Необхідно документувати як шлях успішного виконання, так і шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Необхідно встановити людське схвалення для операцій, які призводять до витрат грошей або змінюють дані у продакшені. Підключення на етапі компіляції не є гарантією повноти функціоналу продукту.

from deepagents.backends.protocol import ExecuteResponse

def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse:
    execution = self._runner.run(self._sandbox.commands.run(command))
    stdout = "\n".join(c.text for c in execution.logs.stdout)
    stderr = "\n".join(c.text for c in execution.logs.stderr)
    output = "\n".join(p for p in (stdout, stderr) if p)
    return ExecuteResponse(output=output, exit_code=execution.exit_code or 0)

.upload_files()

На етапі завантаження файлів необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори мають мати можливість перезапустити крок з відомої точки контролю, не намагаючись здогадатися про прихований стан. Краще використовувати невеликі, перевірювані одиниці коду замість об’ємних скриптів. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану структуру процесу. Встановлюйте людське схвалення для тих кроків, які пов’язані з витратами грошей чи зміною даних у продакшені. Підключення на етапі компіляції не є гарантією повноти бізнес-функціоналу.

from opensandbox.models import WriteEntry
from deepagents.backends.protocol import FileUploadResponse

def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
    entries = [WriteEntry(path=path, data=data, mode=644) for path, data in files]
    try:
        self._runner.run(self._sandbox.files.write_files(entries))
        return [FileUploadResponse(path=p) for p, _ in files]
    except Exception as exc:
        return [FileUploadResponse(path=p, error=str(exc)) for p, _ in files]

.download_files()

На етапі завантаження файлів необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори мають мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Призначте назви файлів, визначте критерії успіху та не допускайте безповідомного часткового завершення. Вимагайте людського схвалення для операцій, які призводять до витрат грошей чи змінюють дані у продакшені. Підключення під час компіляції не є гарантією повноти виконання завдань у бізнес-сенсі.

from deepagents.backends.protocol import FileDownloadResponse

def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
    results = []
    for path in paths:
        try:
            content = self._runner.run(self._sandbox.files.read_bytes(path))
            results.append(FileDownloadResponse(path=path, content=content))
        except Exception as exc:
            results.append(FileDownloadResponse(path=path, error=str(exc)))
    return results

На етапі завантаження файлів необхідно спочатку визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори мають мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Конфігурацію слід тримати окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функціоналу мають знаходитися в одному місці, яке оператори можуть перевірити, не читаючи весь алгоритм.

.kill()

Під час роботи з етапом завершення процесу спочатку складіть опис: необхідні вхідні дані, сигнал про успіх та наслідки часткової невдачі. Такий перелік допоможе зберегти чесність подальших змін у коді. Документуйте як шлях успішного виконання, так і шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Створюйте точки контролю після дорогих операцій. Функція відновлення не повинна знову стягувати плату за один і той самий виклик LLM, коли оператор намагається перезапустити пізніший етап.

def kill(self) -> None:
    self._runner.run(self._sandbox.kill())
    self._runner.shutdown()

Міст синхронної/асинхронної роботи

Під час виконання етапу «Міст синхронної/асинхронної роботи» спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Якщо якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність операцій. Робіть перевірки після дорогих кроків. Функція відновлення не повинна знову оплачувати один і той самий виклик LLM, коли оператор намагається виконати пізнішу операцію.

Агент аналізу даних у сандбоксі

Під час роботи над етапом агента для аналізу даних A спочатку запишіть угоду: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність подальших змін у коді. Розглядайте цей етап як угоду між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте безповідомного часткового виконання завдань. Робіть контрольні пункти після дорогих кроків. Система відновлення не повинна знову стягувати плату за один і той самий виклик LLM, коли оператор перезапускає пізніший етап. Під час роботи над етапом агента для аналізу даних A спочатку запишіть угоду: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність подальших змін у коді. Зберігайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код.

import asyncio
import nest_asyncio
import threading
from datetime import timedelta
from pathlib import Path

from deepagents import create_deep_agent
from deepagents.backends.protocol import ExecuteResponse, FileDownloadResponse, FileUploadResponse
from deepagents.backends.sandbox import BaseSandbox
from langchain.chat_models import init_chat_model

from opensandbox import Sandbox
from opensandbox.config import ConnectionConfig
from opensandbox.models import WriteEntry

# nest_asyncio for running async functions in Jupyter.
nest_asyncio.apply()

IMAGE = "opensandbox/code-interpreter:v1.1.0"
ENTRYPOINT = ["/opt/code-interpreter/code-interpreter.sh"]

backend = MinimalOpenSandboxBackend.create(api_key="SANDBOX_API_KEY")
print("Sandbox ready:", backend.id)
llm = init_chat_model(
    model="gemini-3.5-flash",
    model_provider="google_genai",
    api_key=os.environ["GOOGLE_API_KEY"],
    max_tokens=14750,
    max_retries=5,
)
agent = create_deep_agent(
    model=llm,
    system_prompt=(
        "You are a Python coding assistant with sandbox access. "
        "You specialize in performing data analysis and data visualization with python,"
        "you generate clear reports with good looking charts using seaborn."
    ),
    backend=backend,
)
csv_bytes = Path("customers-1000.csv").read_bytes()
results = backend.upload_files([("/workspace/customers-1000.csv", csv_bytes)])
for r in results:
    if r.error:
        print(f"Upload failed for {r.path}: {r.error}")
    else:
        print(f"Uploaded {r.path}")
result = agent.invoke({
    "messages": "Perform a deep exploratory data analysis on the customers-1000.csv file "
                "and summarize the findings in a markdown report with clear charts."
})
# Deep Exploratory Data Analysis: Customer Acquisition and Profiling
**Dataset:** `customers-1000.csv`
**Analysis Period:** Jan 2020 – May 2022

---

## 1. Executive Summary

This report presents a comprehensive exploratory data analysis (EDA) of a customer database containing 1,000 unique records. The analysis delves into geographical distributions, sign-up temporal trends, domain & technical alignments, and name demographics to uncover actionable insights for strategic growth.

### Key Takeaways
1. **Unprecedented Global Reach:** The customer base is extraordinarily decentralized, spanning **240 countries** across all **7 continents** (including Antarctica). No single country represents more than 1.2% of the customer base. Africa (24.7%) and Asia (22.6%) are the leading regions, followed by Europe (18.5%) and North America (16.1%).
2. **Stable Acquisition Trends:** Customer subscriptions are remarkably stable, averaging roughly **34-35 new customers per month** across 2020 and 2021. This consistency is maintained across all continents year-over-year, indicating a highly standardized, globally distributed customer acquisition channel.
3. **Mid-Week and Weekend Consistency:** Subscriptions are evenly spread across the days of the week, with a minor peak on Friday and Saturday, and a minor trough on Thursday.
4. **B2B / Synthetic Profile Characteristics:** The dataset shows zero domain overlap between customer email domains and company websites (0.0% exact match across 923 unique domains). Combined with the near 1-to-1 ratio of customers to companies, this suggests a highly B2B-centric profile (one representative per enterprise) or synthetically generated profiles with randomized fields.
5. **Standardized TLD Footprint:** The `.com` top-level domain (TLD) dominates both emails (61.2%) and corporate websites (58.8%). The remaining distribution is evenly split among `.org`, `.net`, `.biz`, and `.info`.

---

## 2. Dataset Structure & Data Integrity

The initial dataset contains **1,000 rows** and **12 columns**. An inspection of data integrity reveals excellent completeness:
- **Zero Missing Values:** Every column is 100% populated.
- **Zero Duplicates:** There are no duplicate rows, and the `Customer Id` column contains 1,000 unique identifiers.
- **Data Types:** All columns are stored as object/string types except for `Index` (integer).

### Data Preprocessing & Feature Engineering
To enable deep exploratory analysis, several features were engineered:
1. **Temporal Features:** `Subscription Date` was parsed as a datetime object, allowing the extraction of `Sub_Year`, `Sub_Month`, `Sub_Month_Name`, `Sub_Day_of_Week`, and `Sub_Year_Month` (period).
2. **Geographical Mapping:** Using the `pycountry` and `pycountry-convert` libraries, coupled with a manual fallback dictionary for territories, each of the 240 countries was successfully mapped to its respective **Continent**.
3. **Domain & Technical Profiles:** Email domains (`Email_Domain`), email TLDs (`Email_TLD`), and website TLDs (`Website_TLD`) were extracted to analyze the technical profiling of users.

---

## 3. Geographical Analysis

### Continent-Level Distribution
The geographic reach of this customer base is truly global. Rather than being concentrated in a single dominant market like North America or Europe, customers are spread across all continents:

| Continent | Customer Count | Percentage |
| :--- | :---: | :---: |
| **Africa** | 247 | 24.7% |
| **Asia** | 226 | 22.6% |
| **Europe** | 185 | 18.5% |
| **North America** | 161 | 16.1% |
| **Oceania** | 107 | 10.7% |
| **South America** | 54 | 5.4% |
| **Antarctica** | 20 | 2.0% |

#### Chart 1: Customer Distribution by Continent
![Customer Distribution by Continent](downloads/customer_by_continent.png)

### Country-Level Distribution (Top 15 Countries)
The country-level distribution exhibits a heavy tail, with the 1,000 customers distributed across **240 distinct nations**. This indicates that the average number of customers per country is only **4.17**.

The top countries by customer density are:
- **Liechtenstein:** 12 customers (1.2%)
- **Gabon:** 10 customers (1.0%)
- **China, Bangladesh, Reunion, Nigeria, Luxembourg:** 9 customers each (0.9%)

This extreme dispersion suggests a borderless, digital-first product that appeals universally across jurisdictions without localized geographic bias.

#### Chart 2: Top 15 Countries by Customer Count
![Top 15 Countries by Customer Count](downloads/top_15_countries.png)

---

## 4. Temporal Analysis (Subscription Trends)

... (Trimmed to keep blog (estimated read time short))

Від ноутбуку до пакета PyPi

Процес переходу від ноутбука до певної стадії працює найкраще, якщо його розглядати як вимірювану поверхню. Запишіть один ідеальний приклад роботи, один випадок збою та примітки щодо скасування змін перед розширенням обсягу завдань. Документуйте як успішний, так і відновлювальний сценарії роботи разом. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Зберігайте стан графа у простій та типованій формі. Вкладені блоки приховують інформацію про те, який вузол заповнив яке поле, і ускладнюють продовження роботи після перерв.

!pip install deepagents-opensandbox-backend

Перегляньте

Етап «Перевірте» найкраще функціонує, якщо його розглядати як вимірювану поверхню. Запишіть один ідеальний приклад роботи, один випадок збою та примітку щодо скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям коду замість величезних скриптів. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на складну послідовність операцій. Зберігайте стан графа у простому та типованому вигляді. Вкладені структури приховують інформацію про те, який вузол заповнив яке поле, що ускладнює продовження роботи після перерв.

Чек-лист для експлуатації

Під час роботи за етапом чек-листа для експлуатації спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успішне виконання та наслідки часткового збою. Цей чек-лист допомагає залишати подальші зміни в коді чесними та прозорими.

Записуйте час виконання та витрати на обробку даних поруч із результатами функціональних тестів. Чітке бачення витрат заздалегідь запобігає несподіваним витратам під час переходу з демо-середовища до спільних середовищ.

Чекпоїнт після дорогих кроків. Функція відновлення не повинна знову стягувати плату за той самий виклик LLM, коли оператор перезапускає пізнішу ланку.

Фіксуйте версії залежностей та записуйте дайджест зображення, яке використовувалося під час демонстрації. Відтворюваність краща за індивідуальні знання.

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

Чекпоїнт після дорогих кроків. Функція відновлення не повинна знову стягувати плату за той самий виклик LLM, коли оператор перезапускає пізнішу ланку.

Перед підвищенням рівня стеку заморозьте версії, збережіть ідеальний запис для критичного шляху та підтвердьте кроки відкату. У спільних середовищах потрібні обмеження на швидкість, перевірки приватності та чіткий власник для зміни секретів. Краще надійність, ніж кмітливі одноразові демонстрації.

Примітка до пакету для 43662eb4f13d: не включайте ключі постачальника до репозиторію, встановіть ліміт токенів на сеанс та зберігайте транскрипції поруч із фіксами для оцінки, щоб подальша заміна моделей залишалася порівнянною.

Під час роботи над етапом 0 примітки щодо посилення безпеки спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається при частковій невдачі. Цей перелік допомагає зберігати чесність у подальших змінах коду. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Позначте елементи, визначте критерії успіху та не допускайте беззвучного часткового виконання.

Деталь посилення безпеки 0/814: вимірюйте час виконання, клас помилки та кількість витрачених токенів для цієї примітки, а потім вирішуйте, чи залишити зміни, ґрунтуючись на фіксованому наборі питань, а не на індивідуальних спостереженнях.