diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/1_architecture.md b/content/learning-paths/laptops-and-desktops/openclaw_continuum/1_architecture.md new file mode 100644 index 0000000000..1476a86363 --- /dev/null +++ b/content/learning-paths/laptops-and-desktops/openclaw_continuum/1_architecture.md @@ -0,0 +1,92 @@ +--- +title: Understand the Architecture and Local Data Boundaries +weight: 2 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Transition from Inference to an Assistant + +Running a local LLM gives you a private way to generate text, but it does not yet give you an assistant you can use throughout the day. You still need a convenient way to ask questions, save information, search your documents, retrieve current information, and receive reminders without returning to a terminal each time. + +In this Learning Path, you will deploy [OpenClaw Arm Continuum](https://github.com/odincodeshen/openclaw-arm-continuum) and interact with it from Telegram. This learning path uses a private household AI assistant as an example: you will save a household note, retrieve it later, ask questions about a local document, run an explicit web search, and schedule a proactive notification. The inference, embeddings, documents, vector memory, and task state remain on hardware you control. You can apply the same project architecture to other use cases that require private, locally controlled AI processing. + +This reference implementation uses Telegram as its messaging interface, but the runtime architecture is not limited to Telegram. You can integrate another messaging platform by implementing a gateway that translates its messages and events into the runtime request flow. + +The reference runtime extends OpenClaw by bringing the interaction channel, local AI services, persistent memory, tools, and scheduled tasks together. It provides: + +- **Mobile access through Telegram** for conversations, document uploads, and notifications +- **Local generation through vLLM or llama.cpp**, allowing the inference backend to match the Arm system +- **Persistent local memory and document retrieval** through Ollama embeddings and Qdrant +- **Tool-driven and proactive workflows** through explicit web search, scheduled tasks, and Telegram notifications + +The reference runtime routes each request to the appropriate local model, data source, or tool while keeping persistent AI data under your control. + +{{% notice Note %}} +**OpenClaw provides the foundation for the assistant.** This reference implementation adds vLLM or llama.cpp for local generation and integrates Ollama, Qdrant, browser search, and cron services for memory, tools, and proactive workflows. +{{% /notice %}} + +## Understand the data boundary + +Local-first does not mean that every byte stays offline. Telegram and web search are external network interactions. The important property is that the boundary is explicit and that the core AI data remains under your control. + +| Data or operation | Location | External interaction | +|---|---|---| +| LLM inference | DGX Spark or CPU-only Arm host | Model weights are downloaded during setup | +| Embeddings | Local Ollama service | Model weights are downloaded during setup | +| Vector memory and RAG | Local Qdrant service | None during normal retrieval | +| Uploaded documents | Local runtime workspace | Telegram transports the original upload | +| Cron state and task history | Local workspace and Gateway state | Telegram transports push messages | +| External data lookup | Local skill | Public data service selected by the skill | +| Browser search | Local Playwright worker | Search engine and selected public pages | + +The runtime does not require a public cloud LLM API. However, content sent through Telegram is transported by Telegram, and explicit browser searches reveal the search request to external websites. + +{{% notice Note %}} +This Learning Path uses the default personal runtime configuration, but every exercise uses synthetic or public data. Do not enter real personal, household, or organizational information while following the tutorial. If the host already contains personal runtime data, use the optional demo isolation settings introduced in the next chapter. +{{% /notice %}} + +## Trace the Application Request Path + +In this Learning Path, Telegram requests follow this path: + +```text +Telegram message + -> Reference runtime Telegram gateway + -> AgentRegistry and TaskDispatcher + |-- Memory or RAG -> Ollama and Qdrant + |-- Web search -> Playwright browser worker + |-- External data -> Purpose-built local skill + `-- General chat -> Local LLM endpoint +``` + +The reference runtime can also start a configured workflow automatically, without requiring a new Telegram message from the user: + +```text +Cron schedule + -> Reference runtime cron worker + -> Local skill and local LLM + -> Telegram push notification +``` + +Explicit slash commands remain deterministic. For example, `/search` always selects the browser-search workflow, while a plain-language weather question selects the weather skill. The local model is not allowed to reinterpret an explicit command and silently change its route. + +## Understand the shared API contract across Arm platforms + +To demonstrate that the same project can run across systems with different hardware capabilities, this Learning Path uses an inference engine suited to each platform: + +| Platform | Inference engine | +|---|---| +| NVIDIA DGX Spark | vLLM server | +| Radxa Orion O6 | llama.cpp server | + +Both expose an OpenAI-compatible chat-completions API. The project therefore keeps the same upper-layer workflow on both platforms; only the configured endpoint and model name change. + +This demonstrates the portability of the project architecture: the same local-first workflow can run across different Arm compute configurations by selecting a compatible inference backend. + +## What you've learned and what's next + +You now understand why a local-first assistant is more than a local model, which data remains local, which operations cross the network boundary, and how an OpenAI-compatible endpoint separates the upper-layer runtime from the underlying inference engine. + +Next, you will deploy the baseline runtime on NVIDIA DGX Spark. diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/2_dgx_deploy.md b/content/learning-paths/laptops-and-desktops/openclaw_continuum/2_dgx_deploy.md new file mode 100644 index 0000000000..4350ac6173 --- /dev/null +++ b/content/learning-paths/laptops-and-desktops/openclaw_continuum/2_dgx_deploy.md @@ -0,0 +1,379 @@ +--- +title: Deploy an OpenClaw-based Reference Runtime with vLLM on DGX Spark +weight: 3 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Prepare the DGX Spark Host Environment + +This section assumes Docker Engine, the Docker Compose plugin, the NVIDIA driver, and NVIDIA Container Toolkit are installed on DGX Spark. Cloning the project later in this Learning Path downloads the reference runtime source and configuration, but it does not install Ollama or Qdrant. Prepare these host services before you start the project containers. + +Confirm that the Arm CPU and NVIDIA GPU are visible: + +```bash +uname -m +nvidia-smi +``` + +The expected CPU architecture is: + +```output +aarch64 +``` + +{{% notice Note %}} +This Learning Path uses Docker Engine and Docker Compose to run its services. If Docker is not installed on your DGX Spark, follow the [Install Docker Engine](https://learn.arm.com/install-guides/docker/docker-engine/) guide before continuing. +{{% /notice %}} + +Confirm Docker GPU access: + +```bash +docker run --rm --gpus all ubuntu nvidia-smi +``` + +You do not need to install the vLLM Python package or start a vLLM server directly on the DGX Spark host. The project's `compose.yaml` pulls a container image that already includes vLLM and starts the local inference server for you. The NVIDIA driver and NVIDIA Container Toolkit are still required so that this container can access the GPU. + +## Configure Ollama for Local Embeddings + +Unlike vLLM, Ollama is not included as a service in the project's `compose.yaml`. Install and run Ollama separately on the DGX Spark host before starting the reference runtime. + +Install Ollama using the [official Linux installer](https://docs.ollama.com/linux): + +```bash +curl -fsSL https://ollama.com/install.sh | sh +``` + +The project containers connect to Ollama through the Docker host gateway. Create a systemd override that configures Ollama to listen on the host interfaces: + +```bash +sudo install -d -m 0755 /etc/systemd/system/ollama.service.d +printf '%s\n' '[Service]' 'Environment="OLLAMA_HOST=0.0.0.0:11434"' | \ + sudo tee /etc/systemd/system/ollama.service.d/override.conf +``` + +Confirm the override file: + +```bash +sudo systemctl cat ollama +``` + +The output should include the override: + +```output +# /etc/systemd/system/ollama.service.d/override.conf +[Service] +Environment="OLLAMA_HOST=0.0.0.0:11434" +``` + +Reload systemd and restart Ollama: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now ollama +sudo systemctl restart ollama +``` + +Pull the embedding model used by this Learning Path: + +```bash +ollama pull nomic-embed-text +``` + +Confirm that Ollama lists the model: + +```bash +curl http://127.0.0.1:11434/api/tags +``` + +The response should list `nomic-embed-text:latest` with the `embedding` capability: + +```output +{ + "models": [ + { + "name": "nomic-embed-text:latest", + "model": "nomic-embed-text:latest", + "modified_at": "2026-07-15T16:58:41.999102452+01:00", + "size": 274302450, + "digest": "0a109f422b47e3a30ba2b10eca18548e944e8a23073ee3f3e947efcf3c45e59f", + "details": { + "parent_model": "", + "format": "gguf", + "family": "nomic-bert", + "families": ["nomic-bert"], + "parameter_size": "137M", + "quantization_level": "F16", + "context_length": 2048, + "embedding_length": 768 + }, + "capabilities": ["embedding"] + } + ] +} +``` + +The modification time, size, and digest can differ with the installed model version. + +## Start Qdrant for Persistent Vector Storage + +Create a Docker volume so that vector data remains available when the Qdrant container is replaced: + +```bash +docker volume create openclaw-qdrant-data +``` + +Start Qdrant on the host using the [official container image](https://qdrant.tech/documentation/quick-start/): + +```bash +docker run -d \ + --name openclaw-qdrant \ + --restart unless-stopped \ + -p 6333:6333 \ + -p 6334:6334 \ + -v openclaw-qdrant-data:/qdrant/storage \ + qdrant/qdrant +``` + +The `docker run` command creates the container and is needed only the first time. If `openclaw-qdrant` already exists but is stopped, start it instead: + +```bash +docker start openclaw-qdrant +``` + +Confirm that the Qdrant API responds: + +```bash +curl http://127.0.0.1:6333/collections +``` + +Before the reference runtime creates its collections, the response is similar to: + +```output +{ + "result": { + "collections": [] + }, + "status": "ok", + "time": 0.000064224 +} +``` + +The response time can differ. The empty collection list is expected at this stage; the runtime creates the tutorial collections when you save or ingest content. + +{{% notice Warning %}} +Ollama and Qdrant must be reachable from the project containers, but they should not be exposed to an untrusted network. Use the DGX Spark firewall or another host-level access control to restrict ports `11434`, `6333`, and `6334` to the local host and its Docker networks. +{{% /notice %}} + +## Clone the Reference Repository + +Clone the repository and check out the release used by this Learning Path: + +```bash +git clone https://github.com/odincodeshen/openclaw-arm-continuum.git +cd openclaw-arm-continuum +git checkout v1.2 +``` + +The checked-out tag fixes the source used by this Learning Path even when development continues on `main`. Container images and model artifacts that do not have an explicit version are resolved when you download them and can change independently of the source tag. + +## Configure Private Environment Variables + +Copy the DGX Spark environment template: + +```bash +cp .env.example .env +``` + +Generate a Gateway token: + +```bash +openssl rand -hex 32 +``` + +If you do not already have a Telegram bot, follow the official [Telegram Bot tutorial](https://core.telegram.org/bots/tutorial) to create one before continuing. + +Edit `.env` and set the four private values: + +```text +OPENCLAW_TELEGRAM_BOT_TOKEN= +OPENCLAW_TELEGRAM_ALLOWED_CHAT_IDS= +OPENCLAW_CRON_CHAT_IDS= +OPENCLAW_GATEWAY_TOKEN= +``` + +Set `OPENCLAW_CRON_TIMEZONE` to your local [IANA timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). Scheduled jobs use UTC when this setting is omitted: + +```text +OPENCLAW_CRON_TIMEZONE= +``` + +For example, use `Europe/London`, `America/New_York`, or `Asia/Singapore`. Weather questions in this Learning Path name their location explicitly, so you do not need to configure `OPENCLAW_DEFAULT_WEATHER_LOCATION`. + +{{% notice Note %}} +Do not share your Telegram bot token or chat ID with anyone, and do not include them in screenshots, logs, or public repositories. +{{% /notice %}} + +Only allowlisted chat IDs can send commands to this runtime. + +The main tutorial flow uses the default personal collections: + +```text +personal_tracker_memory +personal_knowledge_base +``` + +You do not need to add collection settings to `.env` for this default path. Use only the synthetic data provided in the exercises. + +{{% notice Note %}} +If this host already contains personal runtime data, or if you are preparing a public demonstration, add the following optional settings to `.env` to isolate the tutorial data: + +```text +OPENCLAW_TRACKER_COLLECTION=demo_tracker_memory +OPENCLAW_KNOWLEDGE_COLLECTION=demo_knowledge_base +OPENCLAW_RUNTIME_LABEL=DGX Spark Demo +``` + +If you choose this option, replace the `personal_*` collection names in later verification commands with the corresponding `demo_*` names. +{{% /notice %}} + +The DGX model used in this Learning Path is text-first. Disable experimental vision routing: + +```text +OPENCLAW_VISION_ENABLED=false +``` + +{{% notice Warning %}} +Never commit `.env`. It contains the Telegram bot token and Gateway authentication token. The repository ignores this file, but you should still verify `git status` before publishing changes. +{{% /notice %}} + +## Initialize and Start the Runtime Stack + +The Gateway runs as user ID `1000` inside its container and needs write access to its persistent state directory. Prepare the directory before starting the stack: + +```bash +mkdir -p gateway-data/state +sudo chown -R 1000:1000 gateway-data +sudo chmod -R u+rwX gateway-data +``` + +Start the complete DGX Spark stack: + +```bash +docker compose --env-file .env -f compose.yaml up -d +``` + +The first start can take several minutes while Docker images and model weights are downloaded and vLLM loads and compiles the model. A running container does not yet mean that its API is ready. + +Verify Service Status and API Readiness: + +```bash +docker compose --env-file .env -f compose.yaml ps -a +docker logs --tail 80 openclaw-vllm +docker logs --tail 80 openclaw-gateway +docker logs --tail 80 openclaw-telegram +docker logs --tail 80 openclaw-cron +``` + +Follow the vLLM log during the first startup: + +```bash +docker logs -f openclaw-vllm +``` + +Wait until the log includes `Application startup complete`. Model download and compilation messages before this point are expected. Press `Ctrl+C` to stop following the log; this does not stop the container. + +Confirm that the model API is ready: + +```bash +curl http://127.0.0.1:8000/v1/models +``` + +Verify that a project container can reach both host services through the Docker host gateway: + +```bash +docker exec openclaw-telegram python -c "import urllib.request; print(urllib.request.urlopen('http://host.docker.internal:11434/api/tags').status)" +docker exec openclaw-telegram python -c "import urllib.request; print(urllib.request.urlopen('http://host.docker.internal:6333/collections').status)" +``` + +Both commands should print HTTP status `200`. + +Confirm the local Gateway dashboard endpoint: + +```bash +curl -I http://127.0.0.1:18789/ +``` + +An HTTP `200` response confirms that the Gateway dashboard is reachable. + +## Run the First Telegram Test + +Open your bot in Telegram and send: + +```text +/help +``` + +The bot should return the OpenClaw command card. Next, send a short general message: + +```text +Explain one benefit of running an AI assistant locally in one sentence. +``` + +Watch the Telegram and vLLM logs while the request is processed: + +![Telegram conversation showing the tutorial prompt and a response from the local reasoning model#center](openclaw_telegram_1.jpg "Telegram response from the local reference runtime") + +```bash +docker logs --tail 10 openclaw-telegram +``` + +The output should look similar to: + +```output +2026-07-17T15:38:47+00:00 [telegram] chat_id= text_chars=69 +2026-07-17T15:38:47+00:00 [runtime] start chat_id= active=1 +2026-07-17T15:38:51+00:00 [runtime] done chat_id= task_id= agent=chat_agent duration_ms=4153 answer_chars=180 +``` + +```bash +docker logs --tail 10 openclaw-vllm +``` + +The recent log should include a successful local completion request similar to: + +```output +(APIServer pid=1) INFO: 172.18.0.7:48686 - "POST /v1/chat/completions HTTP/1.1" 200 OK +``` + +The request appearing in the local logs confirms the runtime path. The model's text alone is not evidence that inference was local. + +## Execute Test Suites + +Run the repository tests from the host: + +```bash +OPENCLAW_OLLAMA_BASE_URL=http://127.0.0.1:11434 \ +OPENCLAW_QDRANT_BASE_URL=http://127.0.0.1:6333 \ +PYTHONPATH=app python3 -m unittest discover -s tests +``` + +The output contains progress dots followed by this result: + +```output +---------------------------------------------------------------------- +Ran 121 tests in 4.256s + +OK (skipped=5) +``` + +The number of tests and the elapsed time can change as the repository evolves. `OK` confirms that the complete suite passed. + +These tests validate command routing, task dispatch, cron parsing, ingestion helpers, and failure handling. They are software behavior tests, not hardware benchmarks. + +## What you've learned and what's next + +You have deployed the personal reference runtime on NVIDIA DGX Spark, connected it to your Telegram bot, verified the local vLLM endpoint, and checked the runtime tests. + +Next, you will use the deployment as a local-first household assistant and confirm that memory is stored in local Qdrant collections. diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/3_household_memory.md b/content/learning-paths/laptops-and-desktops/openclaw_continuum/3_household_memory.md new file mode 100644 index 0000000000..d3ce375d3e --- /dev/null +++ b/content/learning-paths/laptops-and-desktops/openclaw_continuum/3_household_memory.md @@ -0,0 +1,190 @@ +--- +title: Validate Memory Persistence and Routing with Telegram and Qdrant +weight: 4 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Define the Household Test Scenario + +The project provides a customizable runtime for use cases that need private, local-first AI processing. You can adapt its skills, data sources, workflows, and schedules to match your own requirements. To make these capabilities concrete, this chapter uses a shared household assistant as an example rather than prescribing a fixed application. You will save synthetic maintenance information and retrieve it later without sending the memory to a public cloud LLM. You can apply the same architecture to other scenarios that need locally controlled inference, memory, and retrieval. + +Telegram transports the original messages between you and the runtime. After the messages reach your host, Ollama, Qdrant, and the local LLM process the memory workflow without using a public cloud LLM API. + +This tutorial treats household data as shared. It does not implement separate access control for each family member. + +## Store and Query Local Memory + +Send this command to the Telegram bot: + +```text +/mem #home The boiler should be inspected every October. +``` + +The reference runtime performs the following operations: + +```text +Telegram / Mem command + -> Memory skill + -> Ollama embedding + -> Qdrant collection: personal_tracker_memory +``` + +Wait for the confirmation, then retrieve the memory: + +```text +/rag memory: When should the boiler be inspected? +``` + +The response should mention October. + +![Telegram conversation showing the boiler reminder saved with the mem command and retrieved with the rag memory query#center](openclaw_telegram_2.jpg "Saving and retrieving a household memory in Telegram") + + +The retrieval request follows this local path: + +```text +Telegram question + -> Ollama query embedding + -> Qdrant similarity search + -> Retrieved context + -> Local vLLM response + -> Telegram answer +``` + +## Verify Qdrant Vector Collections + +Confirm that the personal memory collection exists: + +```bash +curl http://127.0.0.1:6333/collections/personal_tracker_memory +``` + +The output is similar to: + +```output +{ + "result": { + "status": "green", + "optimizer_status": "ok", + "indexed_vectors_count": 0, + "points_count": 102, + "segments_count": 8, + "config": { + "params": { + "vectors": { + "size": 768, + "distance": "Cosine" + }, + "shard_number": 1, + "replication_factor": 1, + "write_consistency_factor": 1, + "on_disk_payload": true + }, + "hnsw_config": { + "m": 16, + "ef_construct": 100, + "full_scan_threshold": 10000, + "max_indexing_threads": 0, + "on_disk": false + }, + "optimizer_config": { + "deleted_threshold": 0.2, + "vacuum_min_vector_number": 1000, + "default_segment_number": 0, + "max_segment_size": null, + "memmap_threshold": null, + "indexing_threshold": 10000, + "flush_interval_sec": 5, + "max_optimization_threads": null, + "prevent_unoptimized": null + }, + "wal_config": { + "wal_capacity_mb": 32, + "wal_segments_ahead": 0, + "wal_retain_closed": 1 + }, + "quantization_config": null + }, + "payload_schema": {}, + "update_queue": { + "length": 0 + } + }, + "status": "ok", + "time": 0.000305601 +} +``` + +The point count, segment count, and response time depend on the existing data and Qdrant state. A `green` collection with `optimizer_status` set to `ok` confirms that the collection is healthy. The vector size of `768` matches the `nomic-embed-text` embedding configuration. + +The collection metadata does not prove that the boiler reminder was stored. Query the point payload directly to verify the synthetic record: + +```bash +curl -sS -X POST \ + http://127.0.0.1:6333/collections/personal_tracker_memory/points/scroll \ + -H 'Content-Type: application/json' \ + -d '{ + "filter": { + "must": [ + { + "key": "text", + "match": { + "value": "#home The boiler should be inspected every October." + } + } + ] + }, + "limit": 5, + "with_payload": true, + "with_vector": false + }' +``` + +Look for the synthetic boiler memory in the returned payload. This second check verifies the stored content rather than only the health and configuration of the collection. The filter is necessary because a personal collection can already contain other records, so scrolling only the first few points might not return the new reminder. + +This check is important. It verifies the storage location from the data layer instead of trusting the assistant to describe its own architecture. + +## Inspect Active Agents and Task Execution + +Send the following command to the Telegram bot: + +```text +/agents +``` + +The response lists the thin agents registered by the reference runtime, including memory, RAG, browser search, weather, and chat routes. + +To inspect recent tasks, send this command to the Telegram bot: + +```text +/tasks last 5 +``` + +Task history records which agent handled the request, its status, and runtime duration. The dispatcher selects skills and agents while using one configured LLM endpoint. Dynamic routing between multiple models is a possible direction for future development. + +## Test External Skill Integration + +Send a weather question in plain language: + +```text +Cambridge weather tomorrow +``` + +The reference runtime routes the request to the weather skill. Do not add `/search` to this question. An explicit `/search` command selects the general browser worker instead of the dedicated weather route. + +This request crosses the local data boundary because the skill contacts the public [wttr.in](https://wttr.in/) weather service. The local model API is still not replaced by a cloud LLM API. + +## Check your work + +Your household assistant should now: + +1. Save and retrieve the synthetic boiler reminder from Telegram. +2. Store the reminder in `personal_tracker_memory`. +3. Show the selected agent in `/agents` and `/tasks last 5`. +4. Return weather data through the external weather skill. + +## What you've learned and what's next + +You saved and retrieved a synthetic household memory, verified it in Qdrant, and inspected both local and external request paths. Next, you will add document RAG, browser search, and a proactive cron reminder. diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/4_workflows.md b/content/learning-paths/laptops-and-desktops/openclaw_continuum/4_workflows.md new file mode 100644 index 0000000000..a8ff95b943 --- /dev/null +++ b/content/learning-paths/laptops-and-desktops/openclaw_continuum/4_workflows.md @@ -0,0 +1,199 @@ +--- +title: Validate Document RAG, Web Search, and Proactive Tasks +weight: 5 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Ingest and Query Document RAG + +Create a small text file on the device where you use Telegram. The source file can be in any directory that your Telegram client can access. Use the following synthetic tutorial content: + +```text +Household heating maintenance notes + +Inspect the boiler every October. +Clean the heating filter on the first Saturday of every third month. +Keep the service reference number with the maintenance record. +``` + +Save the file as `household-maintenance.txt`. In Telegram, upload the file to your bot. Any caption other than `/tracker` or `/mem` routes the upload to knowledge indexing by default, so use this caption to make the destination explicit: + +```text +/knowledge +``` + +The document follows this path: + +```text +File on the Telegram client device + -> Telegram bot upload + -> DGX Spark workspace/inbox/knowledge/telegram + -> Memory watcher and Ollama embeddings + -> Qdrant collection: personal_knowledge_base +``` + +Telegram transports the uploaded file to the bot. The reference runtime then downloads it to `openclaw-arm-continuum/workspace/inbox/knowledge/telegram/` on DGX Spark. The memory watcher indexes the local copy and writes its chunks to `personal_knowledge_base`. + +{{% notice Note %}} +In this Learning Path, supported Telegram uploads are indexed after they are saved. The review-first upload workflow is planned but is not part of this release. +{{% /notice %}} + +The bot reports the stored filename with a timestamp prefix, similar to `20260717-180500-household-maintenance.txt`. Copy the filename from the response. + +Indexing runs asynchronously. Wait a few seconds, then confirm that the memory watcher processed the file: + +```bash +docker logs --tail 30 openclaw-memory-watcher +``` + +In Telegram, ask a question using the returned filename. Replace `` with the filename reported by the bot: + +```text +/rag When should the heating filter be cleaned? +``` + +The filename is optional for general RAG queries. Without a filename, the reference runtime performs semantic search across its configured memory and knowledge collections. Including the returned filename adds document-specific chunks to the retrieved context and helps scope the answer to this upload. This Learning Path uses the filename so that existing records in the personal collections do not affect the validation result. + +The following screenshot demonstrates the optional general RAG query without a filename. For the reproducible validation in this chapter, use the filename-specific command above. + +![Telegram conversation showing household-maintenance.txt uploaded with the knowledge caption, saved to personal_knowledge_base, and retrieved with a general rag question#center](openclaw_telegram_3.jpg "Uploading and querying a household document in Telegram") + +The answer should mention the first Saturday of every third month. + +The answer verifies retrieval through the application. To verify the stored document directly in Qdrant, filter the collection by the returned filename: + +```bash +curl -sS -X POST \ + http://127.0.0.1:6333/collections/personal_knowledge_base/points/scroll \ + -H 'Content-Type: application/json' \ + -d '{ + "filter": { + "must": [ + { + "key": "file_name", + "match": { + "value": "" + } + } + ] + }, + "limit": 5, + "with_payload": true, + "with_vector": false + }' +``` + +The returned payload should contain chunks from `household-maintenance.txt`. This confirms that the uploaded file is stored and indexed locally rather than relying only on the assistant's answer. + +## Execute Deterministic Web Search + +Use the browser agent when you intentionally need current public information that is not stored in local memory. Send this command to the Telegram bot: + +```text +/search Arm Learning Paths local AI development +``` + +The explicit `/search` prefix selects the browser-search route deterministically: + +```text +Telegram /search command + -> Browser-search agent + -> Local Playwright worker + -> Public search engine and selected pages + -> Local vLLM summary + -> Telegram answer +``` + +The search query and page requests cross the local data boundary. The Playwright worker saves the retrieved public content as Markdown under `workspace/inbox/tracker/web/`, and the local vLLM produces the answer without using a cloud LLM API. + +Confirm that the browser worker handled the request: + +```bash +docker logs --tail 20 openclaw-browser-scraper +``` + +Look for a successful `POST /scrape` request. The Telegram response should cite the retrieved sources and include the path to the saved web Markdown file. + +Finally, send this command in Telegram: + +```text +/tasks last 5 +``` + +Confirm that the search task reports `browser_search_agent`. This verifies the selected agent as well as the external request path. + +## Schedule Proactive Cron Tasks + +Choose a time a few minutes in the future using the runtime timezone configured by `OPENCLAW_CRON_TIMEZONE`. Send this command to the Telegram bot to create a daily tutorial reminder: + +```text +/cron add daily 21:15 Heating check :: Remind the household to review the heating maintenance notes. +``` + +Replace `21:15` with your test time. Then list the job in Telegram: + +```text +/cron list +``` + +The bot returns a job ID, and `/cron list` shows the schedule as `[on]`. At the configured time, the bot starts a new Heating check message for the scheduled task. + +![Telegram conversation showing a daily Heating check cron job created, listed as enabled, and triggered at the configured time#center](openclaw_telegram_4.jpg "Creating and triggering a scheduled reminder in Telegram") + +The job runs only inside the configured due-time window. Creating the job must not execute it immediately. + +After the configured time, verify that the cron worker delivered the scheduled job: + +```bash +docker logs --tail 30 openclaw-cron +``` + +Look for a line containing `[cron] dynamic job sent`, the job ID, and the path to the locally saved cron report. + +To test without waiting, copy the job ID from `/cron list` and send: + +```text +/cron run +``` + +The result should be delivered as a Telegram push message. + +## Inspect Cron From the Gateway Dashboard + +The Gateway dashboard listens on localhost. If you are working directly on the DGX Spark desktop, open: + +```text +http://127.0.0.1:18789/ +``` + +If DGX Spark is remote, create an SSH tunnel from your laptop: + +```bash +ssh -L 18789:127.0.0.1:18789 @ +``` + +Then open `http://127.0.0.1:18789/` locally and enter the `OPENCLAW_GATEWAY_TOKEN` stored in the private `.env` file. + +Confirm that the dashboard and Telegram show the same cron job and run history. + +{{% notice Warning %}} +Keep the Gateway and its admin RPC endpoint behind localhost, an SSH tunnel, or a trusted private network. Do not expose the dashboard directly to the public internet. +{{% /notice %}} + +## Check your work + +You have now validated three runtime paths: + +| User goal | Reference runtime path | +|---|---| +| Answer from a household document | Telegram -> RAG skill -> Qdrant -> local LLM | +| Retrieve current public information | Telegram -> browser-search agent -> Playwright -> local LLM | +| Send a proactive reminder | Cron worker -> configured skill -> Telegram push | + +The LLM is one replaceable part of the application. The local memory, tools, schedules, and interaction paths remain available around it. + +## What you've learned and what's next + +You validated document RAG, explicit browser search, and a proactive reminder for the household assistant. Next, you will move the same workflows to a CPU-only Armv9 system. diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/5_cpu_only.md b/content/learning-paths/laptops-and-desktops/openclaw_continuum/5_cpu_only.md new file mode 100644 index 0000000000..6fda00c65f --- /dev/null +++ b/content/learning-paths/laptops-and-desktops/openclaw_continuum/5_cpu_only.md @@ -0,0 +1,316 @@ +--- +title: Port the Application Workflow to a CPU-Only Armv9 System +weight: 6 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Overview of Cross-Platform Portability + +This runtime architecture is designed to work across Arm systems with different compute configurations. In the previous sections, you deployed it on NVIDIA DGX Spark, a heterogeneous CPU-GPU platform using vLLM for local generation. In this section, you will move the same application workflows to a CIX-based Radxa Orion O6 running Debian 12, with llama.cpp providing local generation on the Armv9 CPU. + +The Telegram interface, local memory and RAG, browser search, scheduled workflows, and deterministic routing remain unchanged. Only the local generation backend changes: + +| Platform | Local generation backend | Runtime API contract | +|---|---|---| +| NVIDIA DGX Spark | vLLM | OpenAI-compatible API | +| Radxa Orion O6 | llama.cpp | OpenAI-compatible API | + +{{% notice Note %}} +These backends were selected to build on the environments used in the earlier chapters and in [Run ERNIE-4.5 Mixture of Experts model on Armv9 with llama.cpp](/learning-paths/cross-platform/ernie_moe_v9/). They are not fixed architecture requirements. You can use another local inference backend that provides a compatible OpenAI chat-completions API. +{{% /notice %}} + +## Verify System Requirements on Armv9 Host + +On Orion O6, confirm the operating system, architecture, CPU features, memory, and disk capacity: + +```bash +uname -a +cat /etc/os-release +lscpu +free -h +df -h / +``` + +Confirm that the host reports `aarch64` and has enough available memory and storage for the selected GGUF model and containers. + +## Prepare llama.cpp and the ERNIE model + +Follow [Set up llama.cpp on an Armv9 development board](/learning-paths/cross-platform/ernie_moe_v9/2_llamacpp_installation/) to install the build dependencies, compile llama.cpp, download the ERNIE-4.5 Thinking Q4 GGUF model, and run the basic inference test on Orion O6. + +After you complete the setup in that Learning Path, continue with the steps below. They use the following installation paths: + +```text +$HOME/llama.cpp/build/bin/llama-server +$HOME/models/ernie-4.5/ERNIE-4.5-21B-A3B-Thinking-Q4_0.gguf +``` + +## Deploy llama.cpp OpenAI-Compatible Server + +Start the server on the host: + +```bash +cd $HOME/llama.cpp + +./build/bin/llama-server \ + --jinja \ + -m $HOME/models/ernie-4.5/ERNIE-4.5-21B-A3B-Thinking-Q4_0.gguf \ + -c 2048 \ + -t 12 \ + --host 127.0.0.1 \ + --port 8080 +``` + +From another shell, inspect the model endpoint: + +```bash +curl http://127.0.0.1:8080/v1/models +``` + +Send a short completion request: + +```bash +curl -sS http://127.0.0.1:8080/v1/chat/completions \ + -H 'Content-Type: application/json' \ + -d '{ + "model": "ernie-o6", + "messages": [{"role":"user","content":"Reply with one sentence about local AI on Arm."}], + "max_tokens": 80, + "temperature": 0.2 + }' +``` + +Do not continue until this local endpoint generates a valid response. + +Press `Ctrl+C` in the server shell after the smoke test. Create a user systemd service so that llama.cpp starts automatically and restarts after a failure: + +```bash +mkdir -p $HOME/.config/systemd/user + +tee $HOME/.config/systemd/user/openclaw-llama.service > /dev/null <<'EOF' +[Unit] +Description=llama.cpp server for the OpenClaw-based runtime +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +ExecStart=%h/llama.cpp/build/bin/llama-server --jinja -m %h/models/ernie-4.5/ERNIE-4.5-21B-A3B-Thinking-Q4_0.gguf -c 2048 -t 12 --host 127.0.0.1 --port 8080 +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=default.target +EOF +``` + +Enable the service and allow it to remain active when you log out: + +```bash +systemctl --user daemon-reload +systemctl --user enable --now openclaw-llama.service +sudo loginctl enable-linger $USER +systemctl --user status openclaw-llama.service --no-pager +``` + +Confirm that the managed endpoint responds: + +```bash +curl http://127.0.0.1:8080/v1/models +``` + +## Provision Supporting Local Services + +Install Ollama on the Orion O6 host: + +```bash +curl -fsSL https://ollama.com/install.sh | sh +sudo systemctl enable --now ollama +``` + +The CPU-only compose file uses host networking, so its containers can reach Ollama through `127.0.0.1:11434`. Pull the embedding model: + +```bash +ollama pull nomic-embed-text +``` + +Confirm that Ollama responds and lists `nomic-embed-text`: + +```bash +curl http://127.0.0.1:11434/api/tags +``` + +Check whether the Qdrant container already exists: + +```bash +docker ps -a --filter name=openclaw-qdrant +``` + +If it already exists, start it: + +```bash +docker start openclaw-qdrant +``` + +Otherwise, create persistent storage and start Qdrant. Bind its ports to localhost: + +```bash +docker volume create openclaw-qdrant-data + +docker run -d \ + --name openclaw-qdrant \ + --restart unless-stopped \ + -p 127.0.0.1:6333:6333 \ + -p 127.0.0.1:6334:6334 \ + -v openclaw-qdrant-data:/qdrant/storage \ + qdrant/qdrant:latest +``` + +Confirm that the local API responds: + +```bash +curl http://127.0.0.1:6333/collections +``` + +## Configure the CPU-Only Runtime Environment + +Clone the same release on Orion O6: + +```bash +cd $HOME +git clone https://github.com/odincodeshen/openclaw-arm-continuum.git +cd openclaw-arm-continuum +git checkout v1.2 +cp .env.arm-cpu-only.example .env +``` + +Create a separate Telegram bot for the CPU-only runtime by following the [Telegram Bot tutorial](https://core.telegram.org/bots/tutorial). Do not reuse the bot token from the running DGX Spark deployment because two polling runtimes using the same bot can compete for Telegram updates. + +Generate a new Gateway token: + +```bash +openssl rand -hex 32 +``` + +Set the new bot and private tokens in `.env`: + +```text +OPENCLAW_TELEGRAM_BOT_TOKEN= +OPENCLAW_TELEGRAM_ALLOWED_CHAT_IDS=, +OPENCLAW_CRON_CHAT_IDS=, +OPENCLAW_GATEWAY_TOKEN= +OPENCLAW_CRON_TIMEZONE= +``` + +Separate multiple allowlisted chat IDs with commas. Both household members can then use the same bot and shared local collections. + +Use the same IANA timezone convention as the DGX Spark deployment. Scheduled jobs use UTC when this setting is omitted. + +Confirm the inference settings: + +```text +OPENCLAW_VLLM_BASE_URL=http://127.0.0.1:8080/v1 +OPENCLAW_VLLM_MODEL=ernie-o6 +OPENCLAW_VISION_ENABLED=false +OPENCLAW_TRACKER_COLLECTION=personal_tracker_memory +OPENCLAW_KNOWLEDGE_COLLECTION=personal_knowledge_base +``` + +Although the environment variable retains the `VLLM` name for compatibility, it represents the configured generation endpoint and can point to llama.cpp. + +Using the same collection names preserves the application contract, but it does not copy Qdrant data from DGX Spark to Orion O6. Each host keeps its own local collection data unless you migrate it separately. + +For a smaller CPU-only context budget, keep search and retrieval compact: + +```text +OPENCLAW_MAX_TOKENS=128 +OPENCLAW_RETRIEVAL_LIMIT=3 +OPENCLAW_SCRAPER_LIMIT=2 +OPENCLAW_WEB_CONTEXT_CHARS=1800 +``` + +## Launch the CPU-Only Application Stack + +Voice transcription is not used in this Learning Path. Keep it disabled in `.env`: + +```text +OPENCLAW_WHISPER_ENABLED=false +``` + +Start the full tutorial stack: + +```bash +docker compose \ + --env-file .env \ + -f compose.arm-cpu-only.yaml \ + --profile web \ + --profile gateway \ + up -d +``` + +Check the services: + +```bash +docker compose --env-file .env -f compose.arm-cpu-only.yaml ps +docker logs --tail 80 openclaw-telegram +docker logs --tail 80 openclaw-memory-watcher +docker logs --tail 80 openclaw-cron +``` + +Confirm that the browser worker can resolve a public hostname: + +```bash +docker exec openclaw-browser-scraper python -c "import socket; print(socket.gethostbyname('duckduckgo.com'))" +``` + +{{% notice Note %}} +If this command cannot resolve the hostname, inspect the Orion host DNS configuration with `cat /etc/resolv.conf`. Then update `OPENCLAW_DNS_SERVER_1` and `OPENCLAW_DNS_SERVER_2` in `.env` with DNS servers that are reachable from your network, restart the stack, and run the check again. +{{% /notice %}} + +## Validate Shared Workflows on CPU + +The previous chapters used a household assistant to validate memory, document retrieval, browser search, and scheduled reminders. In this section, you continue the household scenario on the CPU-only deployment by creating a simple budget assistant that two household members can share. + +Create a file named `budget.txt` on the device where you use Telegram: + +```text +Shared household weekly budget: £120. +``` + +Upload the file to the bot. Any caption other than `/tracker` or `/mem` routes the upload to knowledge indexing by default, so use `/knowledge` as the caption to make the destination explicit. Each allowlisted household member can then add a synthetic expense from their own Telegram chat: + +```text +/mem #budget Groceries: £45. +/mem #budget Household supplies: £20. +``` + +After both entries are saved, either member can ask: + +```text +/rag Based on the shared budget and the saved budget entries, how much remains? +``` + +The response should report that £55 remains. This simple example demonstrates how allowlisted household members can contribute to and query the same local collection. The reference runtime treats this as shared household data and does not provide separate per-member access controls. + +The response alone does not prove which inference backend generated it. Inspect the Telegram runtime log: + +```bash +docker logs --tail 20 openclaw-telegram +``` + +Look for the memory write handled by `memory_agent` and the completed retrieval request handled by `rag_agent`. Then inspect the llama.cpp service log: + +```bash +journalctl --user -u openclaw-llama.service -n 30 --no-pager +``` + +Look for a successful request to `/v1/chat/completions`. The Telegram response and both log entries confirm that the OpenClaw-based workflow is now using llama.cpp for local generation on the Armv9 CPU. + +This simplified example does not reset the budget at the start of each week. You can extend the runtime with a dedicated budget agent and scheduled workflow to manage weekly periods and resets. + +## What you've learned and what's next + +You have moved the OpenClaw-based runtime from DGX Spark to a CPU-only Armv9 system by replacing the inference endpoint rather than rewriting the application. + +Next, you will review the software portability result and identify the current implementation boundaries. diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/6_summary.md b/content/learning-paths/laptops-and-desktops/openclaw_continuum/6_summary.md new file mode 100644 index 0000000000..8e4fda6c2a --- /dev/null +++ b/content/learning-paths/laptops-and-desktops/openclaw_continuum/6_summary.md @@ -0,0 +1,79 @@ +--- +title: Review the Deployment Across Arm Platforms +weight: 7 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Compare Arm Deployment Architectures + +In this Learning Path, you built a local-first household assistant and used Telegram to validate persistent memory, document RAG, browser search, and scheduled notifications. You first ran these workflows with local vLLM inference on NVIDIA DGX Spark, then moved the same application experience to llama.cpp on the Armv9 CPU of a Radxa Orion O6. + +The following comparison shows what stayed the same across the two Arm-based implementations and what changed with the local generation backend: + +| Layer | NVIDIA DGX Spark | Radxa Orion O6 | +|---|---|---| +| Reference runtime services | Same services | Same services | +| User interface | Telegram | Telegram | +| Skills | Memory, RAG, search, weather, cron | Same skills | +| Vector memory | Qdrant | Qdrant | +| Embeddings | Ollama | Ollama | +| Generation API | OpenAI-compatible | OpenAI-compatible | +| Generation engine | vLLM | llama.cpp | +| Inference compute | Arm CPU + NVIDIA GPU | Arm CPU | + +The exercise demonstrates software portability across different compute configurations. Each platform can use model and context settings appropriate to its available compute while preserving the same application-level contract. + +## Review Data Privacy Boundaries + +The runtime keeps the following state under your control: + +- Model inference requests and generated context +- Qdrant memory and RAG collections +- Uploaded document files +- Cron definitions and run history +- OpenClaw task history +- Gateway state + +External boundaries remain visible: + +- Telegram transports messages and uploaded files. +- Weather and browser-search tasks contact public network services. +- Model and container downloads contact external registries during setup. + +For sensitive deployments, you should review network exposure, Telegram suitability, host access, backups, model provenance, and the contents of every enabled tool. + +## Identify Current System Scope + +This Learning Path uses a text-first architecture with deterministic skill routing and one configured local LLM endpoint. + +It does not implement: + +- Dynamic routing across multiple local LLMs +- Multi-agent collaboration or autonomous agent handoffs +- Hardware performance benchmarking + +The thin AgentRegistry and TaskDispatcher route explicit skills and preserve predictable command behavior within this scope. + +## Explore Other Arm Deployment Topologies + +The same endpoint-driven design can support additional deployment shapes: + +- An always-on CPU-only Arm server with a compact local model +- An Arm edge gateway connected to a trusted private-LAN inference server +- A heterogeneous Arm AI workstation hosting larger local models + +Each deployment changes the compute and trust boundary. It should not silently change where personal data is stored or which external services are contacted. + +## Key Takeaways and Next Steps + +You can now: + +- Explain the local and external data boundaries of the reference runtime +- Deploy an operational OpenClaw-based runtime with local vLLM inference on DGX Spark +- Use Telegram memory, RAG, browser search, cron, and Gateway workflows +- Verify local persistence through Qdrant and runtime logs +- Move the same application workflow to llama.cpp on a CPU-only Armv9 platform + +You have moved beyond a local-model demo and built a self-managed OpenClaw-based runtime that can adapt to two different Arm compute configurations. diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/_index.md b/content/learning-paths/laptops-and-desktops/openclaw_continuum/_index.md new file mode 100644 index 0000000000..1407099c2e --- /dev/null +++ b/content/learning-paths/laptops-and-desktops/openclaw_continuum/_index.md @@ -0,0 +1,71 @@ +--- +title: Extend OpenClaw for a Local-First AI Assistant Across Arm Platforms + +description: Extend OpenClaw with local memory, document RAG, browser search, deterministic routing, and proactive scheduling, then move the same local-first runtime from NVIDIA DGX Spark with vLLM to a CPU-only Armv9 system with llama.cpp. + +minutes_to_complete: 120 + +who_is_this_for: This is an advanced topic for developers who want to extend OpenClaw into a customizable local-first assistant with persistent memory, document RAG, explicit browser search, deterministic routing, and proactive scheduling. You will deploy the reference runtime on NVIDIA DGX Spark with vLLM, then move the same workflows to a CPU-only Armv9 system with llama.cpp. + +learning_objectives: + - Explain the local and external data boundaries of an OpenClaw-based runtime + - Deploy and validate the reference runtime with local vLLM inference on NVIDIA DGX Spark + - Verify persistent memory, document RAG, explicit browser search, deterministic routing, and proactive scheduling with Telegram and Qdrant + - Move the same application workflows to a CPU-only Armv9 system through an OpenAI-compatible llama.cpp endpoint + +prerequisites: + - An NVIDIA DGX Spark system with NVIDIA drivers, Docker, and NVIDIA Container Toolkit + - A CPU-only Armv9 system such as Radxa Orion O6 with at least 30 GB of memory + - Administrative access to install Ollama and run Qdrant containers on both systems + - A Telegram bot token and chat ID for the tutorial + - Familiarity with Linux, Docker Compose, and command-line tools + +author: Odin Shen + +generate_summary_faq: true +rerun_summary: false +rerun_faqs: false + +### Tags +skilllevels: Advanced +subjects: ML +armips: + - Cortex-A + - Cortex-X +operatingsystems: + - Linux +tools_software_languages: + - Python + - Docker + - vLLM + - llama.cpp + - Ollama + +further_reading: + - resource: + title: OpenClaw — Personal AI Assistant + link: https://github.com/openclaw/openclaw + type: documentation + - resource: + title: Build a RAG pipeline on Arm-based NVIDIA DGX Spark + link: /learning-paths/laptops-and-desktops/dgx_spark_rag/ + type: Learning Path + - resource: + title: Orchestrate a persistent local AI agent with Hermes on NVIDIA DGX Spark + link: /learning-paths/laptops-and-desktops/dgx_persistent_agent/ + type: Learning Path + - resource: + title: Run ERNIE-4.5 Mixture of Experts model on Armv9 with llama.cpp + link: /learning-paths/cross-platform/ernie_moe_v9/ + type: Learning Path + - resource: + title: OpenClaw Arm Continuum repository + link: https://github.com/odincodeshen/openclaw-arm-continuum + type: documentation + +### FIXED, DO NOT MODIFY +# ================================================================================ +weight: 1 +layout: "learningpathall" +learning_path_main_page: "yes" +--- diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/_next-steps.md b/content/learning-paths/laptops-and-desktops/openclaw_continuum/_next-steps.md new file mode 100644 index 0000000000..c3db0de5a2 --- /dev/null +++ b/content/learning-paths/laptops-and-desktops/openclaw_continuum/_next-steps.md @@ -0,0 +1,8 @@ +--- +# ================================================================================ +# FIXED, DO NOT MODIFY THIS FILE +# ================================================================================ +weight: 21 # Set to always be larger than the content in this path to be at the end of the navigation. +title: "Next Steps" # Always the same, html page title. +layout: "learningpathall" # All files under learning paths have this same wrapper for Hugo processing. +--- diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_1.jpg b/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_1.jpg new file mode 100644 index 0000000000..6e3c7ea170 Binary files /dev/null and b/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_1.jpg differ diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_2.jpg b/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_2.jpg new file mode 100644 index 0000000000..cb8138153c Binary files /dev/null and b/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_2.jpg differ diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_3.jpg b/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_3.jpg new file mode 100644 index 0000000000..2762acafc8 Binary files /dev/null and b/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_3.jpg differ diff --git a/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_4.jpg b/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_4.jpg new file mode 100644 index 0000000000..14a59e9546 Binary files /dev/null and b/content/learning-paths/laptops-and-desktops/openclaw_continuum/openclaw_telegram_4.jpg differ