{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Module 3: Clone, Modify, Deploy\n", "\n", "Clone the Echo environment from the OpenEnv repo, modify it, test locally, and deploy to HF Spaces.\n", "\n", "**Time:** ~25 min · **Difficulty:** Intermediate · **GPU:** Not required\n", "\n", "> **Note:** Deployment to HF Spaces (Step 6) requires a Hugging Face account and token.\n", "> All other steps run locally." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "!pip install -q openenv-core fastmcp fastapi uvicorn\n!git clone --depth=1 -q https://github.com/meta-pytorch/OpenEnv.git 2>/dev/null || true\n\nimport sys, os\nrepo = os.path.abspath('OpenEnv')\nfor p in [repo, os.path.join(repo, 'src')]:\n if p not in sys.path:\n sys.path.insert(0, p)\nprint(\"Setup complete!\")" }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Verify the Hosted Echo Environment\n", "\n", "First, let's confirm the hosted Echo environment works." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from envs.echo_env import EchoEnv\n", "\n", "ECHO_URL = 'https://openenv-echo-env.hf.space'\n", "\n", "with EchoEnv(base_url=ECHO_URL).sync() as env:\n", " result = env.reset()\n", " response = env.call_tool('echo_message', message='ping')\n", " print(f'Sent: ping')\n", " print(f'Received: {response}')\n", " print('The standard Echo returns exactly what you send.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Clone the Echo Environment\n", "\n", "Clone the Space repository to get the full source code." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Copy the echo_env from the cloned OpenEnv repo into a working directory\n", "import shutil, os\n", "\n", "src = os.path.join(os.path.abspath('OpenEnv'), 'envs', 'echo_env')\n", "dst = 'echo-env-modified'\n", "\n", "if os.path.exists(dst):\n", " shutil.rmtree(dst)\n", "shutil.copytree(src, dst)\n", "\n", "# Ensure server/ is a proper Python package so uvicorn can import server.app\n", "# (relative imports inside app.py require a real package, not a namespace package)\n", "for pkg_dir in [dst, os.path.join(dst, 'server')]:\n", " init_file = os.path.join(pkg_dir, '__init__.py')\n", " if not os.path.exists(init_file):\n", " open(init_file, 'w').close()\n", "\n", "print('Copied echo_env to echo-env-modified/')\n", "print('Created __init__.py files for proper package import')\n", "os.listdir(dst)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Explore the Structure\n", "\n", "Every OpenEnv environment follows the same layout." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import glob\n", "files = sorted(glob.glob('echo-env-modified/**/*', recursive=True))\n", "for f in files:\n", " if os.path.isfile(f):\n", " print(f)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Look at the MCP tool definitions in the echo environment\n", "env_file = 'echo-env-modified/server/echo_environment.py'\n", "with open(env_file) as f:\n", " print(f.read())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Modify the Environment\n", "\n", "Let's make a \"Reverse Echo\" — instead of echoing back the message, it reverses it.\n", "\n", "We'll modify the `step()` method in `environment.py`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "env_file = 'echo-env-modified/server/echo_environment.py'\n", "\n", "with open(env_file) as f:\n", " content = f.read()\n", "\n", "print('Original echo_environment.py:')\n", "print(content)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Modify: make echo_message reverse the input\n", "# The MCP tool currently returns `message`; we change it to `message[::-1]`\n", "\n", "modified = content.replace(\n", " 'return message',\n", " 'return message[::-1]',\n", " 1 # Replace only the first occurrence (in echo_message tool)\n", ")\n", "\n", "with open(env_file, 'w') as f:\n", " f.write(modified)\n", "\n", "print('Modified echo_environment.py (echo_message now reverses the input):')\n", "# Show the relevant section\n", "for line in modified.split('\\n'):\n", " if 'echo_message' in line or 'return' in line or '@mcp' in line:\n", " print(f' {line}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Test Locally\n", "\n", "Start the modified server and connect to it.\n", "\n", "> In Colab, we'll start the server as a background process. Locally, you'd run `uv run server` in a separate terminal." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import subprocess\n", "import time\n", "import sys\n", "import os\n", "\n", "# The server app imports from openenv (installed) and envs.echo_env (in OpenEnv repo).\n", "# We run from the echo-env-modified directory so its server/ is importable.\n", "env = os.environ.copy()\n", "env['PYTHONPATH'] = os.pathsep.join([\n", " os.path.abspath('echo-env-modified'),\n", " os.path.abspath('OpenEnv'),\n", " os.path.abspath('OpenEnv/src'),\n", "] + env.get('PYTHONPATH', '').split(os.pathsep))\n", "\n", "server = subprocess.Popen(\n", " [sys.executable, '-m', 'uvicorn', 'server.app:app',\n", " '--host', '0.0.0.0', '--port', '8001'],\n", " cwd='echo-env-modified',\n", " env=env,\n", " stdout=subprocess.PIPE,\n", " stderr=subprocess.PIPE,\n", ")\n", "\n", "# Give it time to start\n", "time.sleep(4)\n", "print(f'Server started (PID: {server.pid})')\n", "\n", "# Check it's healthy\n", "import urllib.request\n", "try:\n", " with urllib.request.urlopen('http://localhost:8001/health', timeout=5) as r:\n", " print(f'Health: {r.read().decode()}')\n", "except Exception as e:\n", " print(f'Health check failed: {e}')\n", " # Print server stderr for debugging\n", " err = server.stderr.read1(1024).decode(errors='replace')\n", " if err:\n", " print(f'Server stderr: {err}')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Test the modified environment\n", "# Since this is an MCP env, we use EchoEnv.call_tool()\n", "from envs.echo_env import EchoEnv\n", "\n", "with EchoEnv(base_url='http://localhost:8001').sync() as env:\n", " result = env.reset()\n", "\n", " test_messages = ['Hello', 'OpenEnv', 'Reverse this!']\n", " for msg in test_messages:\n", " response = env.call_tool('echo_message', message=msg)\n", " print(f'Sent: {msg:20s} -> Received: {response}')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Clean up the server\n", "server.terminate()\n", "server.wait()\n", "print(\"Server stopped.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. Deploy to HF Spaces\n", "\n", "Once your environment works locally, deploy it with `openenv push`.\n", "\n", "```bash\n", "cd echo-env-modified\n", "openenv push --repo-id YOUR_USERNAME/reverse-echo-env\n", "```\n", "\n", "Your environment is now live at:\n", "- **API:** `https://YOUR_USERNAME-reverse-echo-env.hf.space`\n", "- **Web UI:** `https://YOUR_USERNAME-reverse-echo-env.hf.space/web`\n", "- **Docs:** `https://YOUR_USERNAME-reverse-echo-env.hf.space/docs`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Uncomment and run to deploy (requires HF token)\n", "# !cd echo-env-modified && openenv push --repo-id YOUR_USERNAME/reverse-echo-env" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. Connect to Your Deployed Environment\n", "\n", "After deployment, install the client and connect:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Uncomment after deploying\n", "# !pip install -q git+https://huggingface.co/spaces/YOUR_USERNAME/reverse-echo-env\n", "#\n", "# with EchoEnv(base_url=\"https://YOUR_USERNAME-reverse-echo-env.hf.space\").sync() as env:\n", "# result = env.reset()\n", "# result = env.step(EchoAction(message=\"Deployed!\"))\n", "# print(f\"Response from your Space: {result.observation}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 8. Docker Deployment (Alternative)\n", "\n", "You can also pull and run the Docker image locally:\n", "\n", "```bash\n", "# Pull from HF registry (after deploying)\n", "docker pull registry.hf.space/YOUR_USERNAME-reverse-echo-env:latest\n", "docker run -d -p 8001:8000 registry.hf.space/YOUR_USERNAME-reverse-echo-env:latest\n", "\n", "# Or build from source\n", "cd echo-env-modified\n", "docker build -t reverse-echo:latest -f server/Dockerfile .\n", "docker run -d -p 8001:8000 reverse-echo:latest\n", "```\n", "\n", "Connect the same way:\n", "```python\n", "with EchoEnv(base_url=\"http://localhost:8001\").sync() as env:\n", " result = env.reset()\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "\n", "What you did:\n", "1. Cloned an existing environment from HF Spaces\n", "2. Explored its structure (models, client, server)\n", "3. Modified the environment logic (echo → reverse echo)\n", "4. Tested locally with uvicorn\n", "5. Deployed to HF Spaces with `openenv push`\n", "\n", "The workflow is always: **clone → modify → test → deploy**.\n", "\n", "**Next:** [Module 4](../module-4/README.md) — Building an environment from scratch." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11.0" } }, "nbformat": 4, "nbformat_minor": 4 }