Startseite / Artikel

Ausführung von Deep Agents in einem lokalen OpenSandbox-Backend

Wie man OpenSandbox über BaseSandbox in Deep Agents integriert, synchronen und asynchronen Code verbindet sowie einen Datenanalyse-Agenten sicher in einer lokalen Docker-Sandbox ausführt.

Verlag: REACTAPP.TOP15 Min. Lesezeit3403 Wörter

Quellen: Upgrade Your Deep Agent With a Local Open-Source Sandbox

Warum Agenten einen sicheren Ort zum Ausführen von Code benötigen

Chat-Assistenten wie ChatGPT, Claude, Gemini und Qwen nehmen Uploads wie Bilder, PDFs und Tabellenkalkulationen entgegen. Viele Anfragen an diese Dateien – beispielsweise die Beschreibung eines Bildes, die Zusammenfassung eines PDFs oder das Herausziehen eines Auszugs aus einem Dokument – liegen im natürlichen Aufgabenbereich eines Sprachmodells. Gebt einem solchen Modell jedoch eine .csv-Datei und bittet es um eine umfassende statistische Analyse mit Diagrammen, reicht reines Logisches Denken nicht aus. Die Aufgabe erfordert tatsächliche Datenverarbeitung, Berechnungen sowie die Erstellung von Diagrammen.

Ein LLM kann diese Mathematik oder das Zeichnen nicht selbst ausführen, kann aber ein Python-Skript schreiben, das dazu pandas, NumPy und Matplotlib verwendet. Nach dem Ausführen des Skripts besteht die weitere Aufgabe des Modells darin, die Ergebnisse in einen lesbaren Bericht umzuwandeln. Agierende Tools sind es, die das Ausführen dieser Skripte möglich machen.

Doch die direkte Ausführung von von KI verfasstem Code auf dem eigenen Rechner ist keine gute Idee. Die unüberwachte Ausführung stellt ein ernstes Sicherheits- und Stabilitätsrisiko dar: Sie kann die Systemkonfiguration ändern, wichtige Dateien löschen oder personenbezogene Daten preisgeben. Deshalb gibt es Sandboxen – sichere, isolierte Umgebungen, in denen der Agent seinen Code ausführen kann, ohne auf das zugrunde liegende Betriebssystem zuzugreifen. Dieser Text konzentriert sich auf Sandboxen für die Deep Agents Python-Bibliothek.

Was eine Sandbox ist und warum Agenten eine benötigen

Hier bedeutet ein Sandbox-Umfeld eine abgeschottete Umgebung, in der der Code typischerweise in einem Container ausgeführt wird, ohne jeglichen Kontakt zum Rest des Computers. Sein Dateisystem und seine Prozesse gehören ihm selbst, und es gibt keinen Weg zu den Dateien, Zugangsdaten oder dem Netzwerk des Host-Computers, es sei denn, es wird absichtlich Zugang gewährt. Entwickler können dies als praktischen Vorteil betrachten, doch für einen autonomen Agenten ist es nahezu eine Notwendigkeit – und die Gründe sind praktischer Natur:

  • Der Agent wählt den Code, nicht der Benutzer. Selbst ein gut verhaltener Agent löscht manchmal die falsche Datei oder führt einen Befehl aus, den niemand beabsichtigt hat, und ohne Isolierung trifft der Schaden den echten Computer.
  • Prompt-Injektion ist real. Wenn der Agent unzuverlässigen Inhalt liest, wie beispielsweise eine Webseite oder eine Datei von jemand anderem, kann eine geschickt formulierte versteckte Anweisung versuchen, seine weitere Vorgehensweise zu beeinflussen. Eine Sandbox begrenzt den Schaden auf einen einmaligen Container.
  • Reine Startumgebungen helfen. Jede Aufgabe lässt sich leichter verstehen, wenn sie in einer bekannten, leeren Umgebung beginnt und keinen Zustand aus früheren Ausführungen übernimmt.
  • Parallel laufende Agenten. Durch Isolation kann ein pip install eines Agenten nicht mit dem eines anderen kollidieren, und ein abgestürzter Sandbox-Container beeinträchtigt nicht den gesamten Prozess.
  • Ressourcenlimits. Ein Agent, der in einer Endlosschleife gefangen ist oder ein ganzes Datensatz zweimal in den Speicher lädt, sollte die Ressourcen eines temporären Containers verbrauchen und nicht die Ihres PCs.
  • Deshalb betrachten Frameworks, die auf die Programmierung von Agenten abzielen – einschließlich Deep Agents – den Sandbox als ein Konzept erster Klasse. Deep Agents stellt eine Abstraktion namens BaseSandbox bereit sowie offizielle Integrationen mit einigen Cloud-Sandbox-Anbietern. Wenn man den Agenten auf einen dieser Anbieter richtet, erhält er einen echten, isolierten Ort zum Ausführen von Code.

    Sie alle werden in der Cloud gehostet und nach Minute oder nach Nutzung abgerechnet. Das eignet sich für die Produktion, ist aber für das Lokale Erstellen, Testen oder Vorzeigen eines Agenten überflüssig.

    OpenSandbox als lokale Alternative

    OpenSandbox ist eine kostenlose, open-source Sandbox-Umgebung, die auf Ihrem eigenen Docker läuft. Sie bietet dieselben grundlegenden Funktionen wie die kostenpflichtigen Optionen:

    • Starten eines isolierten Containers.
    • Ausführen von Befehlen darin.
    • Lesen und Schreiben von Dateien dort.
    • Später wieder Abbau des Containers.

    Starten Sie den Server des Control-Planes lokal, richten Sie ihn auf Docker aus – dann erscheint ein lokaler Endpunkt, der bei Bedarf Sandboxes erstellt und verwaltet. Niemand verkauft es als gehostetes Produkt. Es handelt sich um Infrastruktur, die Sie selbst betreiben, was sich für lokale Entwicklung, Tests sowie Demonstrationen wie diese eignet. Übrig bleibt nur noch die Verbindung zu Deep Agents, wobei dabei keine Integration mit OpenSandbox vorhanden ist.

    Der BaseSandbox-Vertrag

    BaseSandbox ist eine abstrakte Klasse, und die Integration beschränkt sich auf vier Member.

    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]: ...
    
    1. execute führt einen Befehl im Sandbox-Umfeld aus und gibt eine ExecuteResponse zurück.
    2. id ist eine eindeutige Identifikationsnummer für den Sandbox-Backend.
    3. upload_files sendet mehrere Dateien in den Sandbox.
    4. download_files lädt mehrere Dateien aus dem Sandbox herunter.

    Sobald diese vier Funktionen vorhanden sind, erstellt Deep Agents selbst ls, read_file, write_file, edit_file, glob und grep auf Basis von execute(). Für sie muss nichts weiter programmiert werden. Dies stellt die vollständige Integrationsoberfläche dar, was bedeutet, dass jede Sandbox, die in der Lage ist, Shell-Befehle auszuführen sowie Dateien hinein- und herauszuschieben, geeignet ist. Der Ansatz scheint einfach, bis mit dem eigentlichen Programmieren die Fehler auftauchen.

    Wie OpenSandbox funktioniert

    OpenSandbox verwendet Docker-Container, um Sandboxes zu erstellen und zu verwalten. Sein Python-SDK kann Sandboxing-Umgebungen erstellen und verwalten, Shell-Befehle ausführen, mit Dateien arbeiten und Ressourcen überwachen. Für die Einrichtung ist Docker installiert und in Betrieb sein sowie der Server selbst:

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

    Danach verbindet sich die SDK mit dem laufenden Server und stellt alles zur Verfügung, was OpenSandbox tun kann.

    Die Integrationsklasse

    Das ist der Rahmen, der gemäß der BaseSandbox-Abstraktion ausgefüllt werden muss. Die Methoden werden nacheinander hinzugefügt.

    create()

    class MinimalOpenSandboxBackend(BaseSandbox):
        def __init__(self, sandbox: Sandbox, runner: AsyncRunner):
            self._sandbox = sandbox
            self._runner = runner
    

    __init__ ist einfach. Er enthält zwei Objekte, von denen alles Weitere abhängt: die aktive Sandbox-Verbindung zum Server sowie den AsyncRunner, der diese steuert. Keines dieser Objekte wird manuell erstellt, da beide aus einer Factory-Methode hervorgehen. Die Klasse weist außerdem mehr Methoden auf, als BaseSandbox erfordert – insbesondere eine Klassenmethode .create(). Der Grund wird klarer, wenn man sieht, wie ein Sandbox-Objekt tatsächlich entsteht.

    Sowohl die Sandbox-Verbindung als auch der Hintergrundläufer müssen bereits vorhanden sein, bevor __init__ sie nutzen kann – deshalb ist create() ein eigener Schritt. Hier ist der Code.

    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)
    

    Zwei Details fallen besonders auf. domain="localhost:8080" verbindet dies mit dem zuvor mit uvx opensandbox-server gestarteten Server. timeout=timedelta(minutes=30) dient als Sicherheitsnetz: Falls man vergisst, kill() aufzurufen, wird der Sandbox dennoch selbst beendet, anstatt im Hintergrund zu bleiben. Achten Sie auch auf IMAGE und ENTRYPOINT, die mit der verwendeten OpenSandbox-Version kompatibel sein müssen.

    Über den Ausführender: Sandbox.create ist eine Koroutine, doch BaseSandbox.create muss stattdessen ein gewöhnliches Objekt zurückgeben und nicht etwas, auf das gewartet werden muss. Bis die Details bekannt sind, betrachten Sie runner.run(coro) als „Warten, bis dieser asynchrone Aufruf abgeschlossen ist, und mir anschließend das zurückgeben, was er ergeben hat.“ Das erklärt, warum Sandbox.create(...) an runner.run(...) übergeben wird, anstatt direkt darauf gewartet zu werden. Wie AsyncRunner im Inneren funktioniert, wird etwas später erläutert.

    id

    Die Erstellung ist abgeschlossen, sodass die verbleibenden Methoden sich mit einer bereits laufenden Sandbox beschäftigen.

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

    Es gibt die ID des zugrunde liegenden Sandboxes zurück. BaseSandbox verlangt sie als Eigenschaft, und sie ist in Logs praktisch.

    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)
    

    Ursprünglich scheint das einfach zu sein. OpenSandbox liefert die Befehlsausgabe als Liste von Blöcken aus, jeweils einer pro Zeile, wobei die Zeilenumbrüche bereits entfernt wurden. Die Antwort muss daher ordnungsgemäß neu aufgebaut werden, damit der Agent sie später verarbeiten kann. Auch execute() wird nicht direkt aufgerufen. ls, glob und grep in Deep Agents basieren alle darauf und lesen die Ausgabe Zeile für Zeile ein. Wenn man ihnen eine fehlerhafte Antwort gibt, finden sie nichts und liefern leise leere Ergebnisse zurück. ls gibt [] aus, es gibt keinen Fehlerverlauf, der den fehlerhaften join kennzeichnet, und die Sandbox erscheint leer, obwohl das nicht der Fall ist.

    Es ist wichtiger, als es scheint, denn execute() dient nicht nur der direkten Verwendung. Er bildet die Grundlage für ls, glob und grep in Deep Agents, wobei alle diese Funktionen die Ausgabe Zeile für Zeile verarbeiten. Eine schlecht formatierte Antwort führt dazu, dass keine Übereinstimmungen gefunden werden und stumm leere Ergebnisse zurückgegeben werden. ls gibt dann [] aus, ohne einen Rückverfolgungspfad zum fehlerhaften join anzugeben, und der Sandbox erscheint leer, obwohl das nicht der Fall ist.

    Der Rest ist einfacher: stdout und stderr werden zusammengefügt, wobei das leere Element übersprungen wird, und ein fehlender Exit-Code gilt als Erfolg (0) anstelle von None.

    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]
    

    Jedes (Pfad, Bytes)-Paar wird in eine WriteEntry mit normalen Berechtigungen eingebettet (der Besitzer kann lesen und schreiben, andere nur lesen), und die gesamte Gruppe wird über einen einzigen Aufruf von write_files() gesendet.

    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
    

    Der entscheidende Unterschied liegt bei read_bytes im Vergleich zu read_file. Das SDK bietet beide Funktionen an, doch read_file geht davon aus, dass es sich um UTF-8-Text handelt. Ein Sandbox-Umfeld, das den Code eines LLM ausführt, kann nicht erkennen, ob die nächst angeforderte Datei ein CSV-Datei oder ein frisch generierter Diagramm ist; daher ist read_bytes die einzige sichere Wahl für beide Fälle.

    kill()

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

    Die Aufräumfunktion erledigt zwei wichtige Aufgaben. Zunächst wird der Sandbox-Container beendet, anschließend wird explizit der Hintergrundthread des Ausführers abgeschaltet. Wird der zweite Schritt übersprungen, bleibt der Thread zusammen mit dem darin enthaltenen Ereigniszyklus weiterhin bestehen, nachdem der Sandbox verschwunden ist.

    Das ist die gesamte Klasse: Zwei Lebenszyklusmethoden (create, kill) umgeben die vier Methoden, die BaseSandbox erfordert (id, execute, upload_files, download_files).

    Die Synchron-/Asynchron-Brücke

    Ein Punkt wurde noch nicht erklärt: self._runner.run(...) – der Mechanismus, der jeden Aufruf des asynchronen SDKs für BaseSandbox als synchron erscheinen lässt.

    BaseSandbox ist synchron. execute(), upload_files() und download_files() sind einfache Funktionsaufrufe ohne await. Das OpenSandbox SDK hingegen ist asynchron: Sandbox.create(), sandbox.commands.run() und sandbox.files.write_files() sind allesamt Koroutinen. AsyncRunner dient dazu, diese Lücke zu schließen.

    Der Ansatz ist einfach. Es wird eine Eventloop erstellt, dieser wird einer eigenen Hintergrundthread zugeordnet und solange am Laufen gehalten, wie die Sandbox existiert. Alles Weitere verwendet ausschließlich run(), welcher eine Koroutine über run_coroutine_threadsafe an diese Loop übermittelt, den aufrufenden Code blockiert, bis sie abgeschlossen ist, und das Ergebnis zurückgibt. Für execute() sieht self._runner.run(some_coroutine) genauso aus wie ein gewöhnlicher Funktionsaufruf.

    Eine einzige, lang lebende Schleife anstelle einer neuen pro Aufruf ermöglicht es außerdem der Verbindung, als eine kontinuierliche Sitzung zu fungieren, anstatt bei jedem execute() abgebaut und neu aufgebaut zu werden. shutdown() kümmert sich um die Aufräumarbeiten, indem es die Schleife stoppt und den Thread sauber beendet, weshalb kill() einfach darauf verweist.

    Ein Datenanalyse-Agent in der Sandbox

    Zur Überprüfung der Integration startet ein Jupyter-Notebook mit der Einstellung von Umgebungsvariablen und Konstanten, dem Import von Paketen und vor allem mit der Erstellung eines MinimalOpenSandboxBackend-Objekts. Dafür ist eine von Ihnen selbst erstellte API-Schlüssel erforderlich, die im ~/.sandbox.toml-Datei gesetzt wird.

    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)
    

    Danach kommt ein Chat-Modell. Lokale Modelle über Ollama würden ebenfalls funktionieren, doch das Modell muss weiterhin agierende Aufgaben bewältigen. In diesem Fall wird ein Gemini Flash-Modell verwendet.

    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,
    )
    

    Dann der Agent selbst.

    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,
    )
    

    Alles, was bisher entwickelt wurde, kommt im Argument backend=backend zum Tragen. Aus diesem einzigen Objekt generiert Deep Agents selbst ls, read_file, write_file, glob und grep, wobei die benutzerdefinierte Funktion execute() als Grundlage dient.

    Anschließend werden einige echte Daten direkt mit upload_files() geladen. Das Datensatzset besteht aus 1.000 fiktiven Kunden.

    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}")
    

    Dann erhält der Agent die eigentliche Aufgabe.

    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."
    })
    

    Zu diesem Zeitpunkt arbeitet der Agent allein. Mit seinen Tools und der Infrastruktur kann er pandas- und seaborn-Code schreiben, diesen über execute() in der Sandbox ausführen, Diagramme im Dateisystem der Sandbox speichern und weiter iterieren, bis ein vollständiger Markdown-Bericht entstanden ist.

    Es bleibt jedoch ein Problem: Ein Teil des Berichts bleibt in der Sandbox. Die darin enthaltenen Markdown-Elemente verweisen auf Diagramme mit Pfaden, die nur in der Sandbox existieren und auf dem Host nicht verfügbar sind. Um den Bericht lesbar zu machen, sind zwei Schritte erforderlich: Zunächst werden die Diagrammbilder mit download_files() heruntergeladen, anschließend werden die Markdown-Pfade zu den Bildern so geändert, dass sie auf die lokalen Kopien verweisen. Ein robusterer Ansatz besteht darin, dass der Agent strukturierte Ausgaben zurückgibt – beispielsweise ein Berichtsfeld sowie eine explizite Liste der Bildpfade – anstelle der Suche in freiformigem Markdown nach Links. Die Nachverarbeitung besteht dann lediglich darin, alle aufgelisteten Pfade herunterzuladen und sie auszutauschen.

    Danach folgt, was die Sandbox aus dem rohen CSV erzeugt hat.

    # 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))
    

    Ein gekürzter Blick auf den generierten Bericht:

    Von Notebook zu einem PyPI-Paket

    Ein Notebook zeigt, wie die einzelnen Komponenten zusammenpassen, doch ein Paket, das jeder mit pip installieren kann, ist nützlicher. Die Entwicklung von deepagents_opensandbox_backend bedeutete, alles zu reparieren, was nur dann versagte, wenn andere Personen es in Konfigurationen ausführten, die der Autor nie getestet hatte. Die öffentliche Schnittstelle blieb unverändert, mit denselben BaseSandbox-Methoden sowie denselben create()- und kill()-Funktionen. Die Änderungen fanden im Hintergrund statt:

    • Ein gemeinsamer Schleifenmechanismus. Die Backend-Implementierungen teilen nun einen einzigen modulweiten _BackgroundLoop und führen nicht mehr jeweils eigene Threads aus.
    • Standardmäßige Sicherheitshinweisung. Da die API nicht authentifiziert ist, löst create() nun eine InsecureSandboxWarning aus, wenn keine api_key angegeben wird, während es zuvor stumm verbunden ist.
    • Lösung für Docker Desktop. Unter Docker Desktop führt eine direkte Verbindung zu einem Container-Port häufig zu einem Timeout. Durch die Einstellung von use_server_proxy=True wird der Datenverkehr über den Server geleitet, wodurch das Problem umgangen wird.
    • Prüfung der Pfade. upload_files und download_files lehnen relative Pfade von vornherein ab, sodass der Server sie nicht heimlich in falsche Verzeichnisse umleiten kann.
    • Klareere Fehlermeldungen. Der Client überprüft die Dateiinformationen, um zwischen „Genehmigung verweigert“ und „Datei nicht gefunden“ zu unterscheiden, und weist jede Meldung dem entsprechenden Fehlercode zu, anstatt irreführende Rohfehler weiterzuleiten.
    • Funktionsfähige Zeitlimits. Das Argument timeout wurde früher ignoriert. Jetzt wird es an den Server übergeben, um abgeschlossene Befehle zu beenden, anstatt sie nach Aufgabe des Clients unerledigt zu lassen.
    • Entfernte manuell geschriebene asynchrone Methoden. Echte asynchrone Versionen wie aexecute führten zu stummen, leeren Ausgaben innerhalb von LangGraphs ToolNode; daher wurden sie entfernt, und der Code greift nun auf die normalen synchronen Methoden zurück, die in einem Threadpool ausgeführt werden.

    Dieser Unterschied trennt eine Demo, die auf einem Laptop funktioniert, von einem Paket, das jeder mit pip install nutzen kann. Eine Konformitätsprüfungsreihe, die gegen die Standard-Sandbox-Tests von Deep Agents selbst ausgeführt wird, ist dabei enthalten, und die Installation sieht so aus:

    !pip install deepagents-opensandbox-backend
    

    Es handelt sich um dieselbe Integration wie oben beschrieben, wobei die scharfen Kanten abgeschliffen wurden.

    Verwandte Artikel

  • AgentSRE: Composable AI Agents für autonome Incidentsbehebung auf AWS — Wie eine AWS-Plattform einen Incident von einem CloudWatch-Alarm bis zur Lösung mithilfe eines DevOps-Agenten, LangGraph-Remedierungsagenten, menschlicher Freigabe und ServiceNow-Tickets abwickelt.
  • Was ein Schema Ihrem Agenten nicht mitteilen kann: Open Knowledge Format in BigQuery — Ein Experiment mit BigQuery und ADK, das zeigt, wie Markdown-Wissenspakete in Git die Antworten eines Agenten stabilisierten und einen GMV-Fehler von 29 % behebten.
  • Vergleich von CrewAI, AutoGen, LangGraph, OpenAI Agents SDK und smolagents — Ein Vergleich von fünf Open-Source-Agent-Frameworks aus Sicht eines Ingenieurs hinsichtlich Orchestrierung, Speicher, Tools, Bewertungsmethoden und Überwachbarkeit sowie häufig auftretender Fehlermuster.