How to Build a QwenPaw Agent Workspace with Custom Skills, Model Providers, Console Access, and Streaming API Testing
This tutorial provides a step-by-step guide to setting up a QwenPaw agent workspace in Google Colab, including installation, configuration, authentication, connecting model providers (OpenAI, OpenRouter, DashScope, DeepSeek, Gemini), creating custom skills and local knowledge files, launching the console with optional Cloudflare tunnel, and testing the streaming chat API.
In this tutorial, we implement a QwenPaw workflow that provides a practical environment for building and testing an agent-powered assistant. We install and initialize QwenPaw, configure its working directory, set up authentication, connect optional model providers via Colab secrets, and create a structured workspace with custom skills and local knowledge files. We also launch the QwenPaw Console via a Colab-accessible URL, expose it through an optional Cloudflare tunnel, and test the streaming chat API programmatically, enabling us to use QwenPaw both as an interactive assistant and as an API-driven agent framework.
Copy CodeCopiedUse a different Browser
import os import sys import json import time import uuid import shlex import signal import shutil import socket import secrets import pathlib import subprocess from datetime import datetime RESET_QWENPAW = False PORT = int(os.environ.get("QWENPAW_COLAB_PORT", "8088")) ROOT = pathlib.Path("/content/qwenpaw_colab") WORKING_DIR = ROOT / "working" SECRET_DIR = ROOT / "secrets" LOG_DIR = ROOT / "logs" WORKSPACE_DIR = WORKING_DIR / "workspaces" / "default" PID_FILE = ROOT / "qwenpaw_app.pid" APP_LOG = LOG_DIR / "qwenpaw_app.log" if RESET_QWENPAW and ROOT.exists(): shutil.rmtree(ROOT) for p in [ROOT, WORKING_DIR, SECRET_DIR, LOG_DIR, WORKSPACE_DIR]: p.mkdir(parents=True, exist_ok=True) os.environ["QWENPAW_WORKING_DIR"] = str(WORKING_DIR) os.environ["QWENPAW_SECRET_DIR"] = str(SECRET_DIR) os.environ["QWENPAW_AUTH_ENABLED"] = "true" os.environ["QWENPAW_AUTH_USERNAME"] = os.environ.get("QWENPAW_AUTH_USERNAME", "admin") os.environ["QWENPAW_LOG_LEVEL"] = os.environ.get("QWENPAW_LOG_LEVEL", "info") os.environ["QWENPAW_SKILL_SCAN_MODE"] = os.environ.get("QWENPAW_SKILL_SCAN_MODE", "warn") os.environ["QWENPAW_TOOL_GUARD_ENABLED"] = os.environ.get("QWENPAW_TOOL_GUARD_ENABLED", "true") password_file = SECRET_DIR / ".colab_ui_password" if not password_file.exists(): password_file.write_text("qpw-" + secrets.token_urlsafe(18), encoding="utf-8") os.environ["QWENPAW_AUTH_PASSWORD"] = password_file.read_text(encoding="utf-8").strip() def run(cmd, check=False, env=None, cwd=None, stream=False): if isinstance(cmd, str): display_cmd = cmd shell = True else: display_cmd = " ".join(shlex.quote(str(x)) for x in cmd) shell = False print(f"\n$ {display_cmd}") if stream: proc = subprocess.Popen(cmd, shell=shell, env=env, cwd=cwd, text=True) rc = proc.wait() if check and rc != 0: raise RuntimeError(f"Command failed with exit code {rc}: {display_cmd}") return rc, "" out = subprocess.run( cmd, shell=shell, env=env, cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) print(out.stdout[-4000:]) if check and out.returncode != 0: raise RuntimeError(f"Command failed with exit code {out.returncode}: {display_cmd}") return out.returncode, out.stdout def port_open(host="127.0.0.1", port=8088, timeout=0.5): try: with socket.create_connection((host, port), timeout=timeout): return True except OSError: return False def wait_for_port(port, seconds=90): start = time.time() while time.time() - start = (3, 10), "QwenPaw needs Python 3.10+." pip_spec = os.environ.get("QWENPAW_PIP_SPEC", "qwenpaw") run([sys.executable, "-m", "pip", "install", "-q", "-U", "pip", "setuptools", "wheel"], check=False) run([sys.executable, "-m", "pip", "install", "-q", "-U", pip_spec, "requests"], check=True) try: import requests except Exception: run([sys.executable, "-m", "pip", "install", "-q", "-U", "requests"], check=True) import requests
We start by importing all required Python modules and setting up the main directories for the QwenPaw Colab workspace. We configure environment variables for authentication, logging, working paths, and secure access to the QwenPaw Console. We also define helper functions to run shell commands, check ports, stop old app processes, and read API keys from Colab secrets or environment variables.
Copy CodeCopiedUse a different Browser
if not (WORKING_DIR / "config.json").exists(): run(qwenpaw_cmd("init", "--defaults"), check=False) else: print("QwenPaw working directory already initialized:", WORKING_DIR) provider_candidates = [ { "env": "OPENAI_API_KEY", "provider_id": "openai", "name": "OpenAI", "base_url": "https://api.openai.com/v1", "model": os.environ.get("QWENPAW_MODEL", "gpt-4o-mini"), "chat_model": "OpenAIChatModel", "prefix": "sk-", }, { "env": "OPENROUTER_API_KEY", "provider_id": "openrouter", "name": "OpenRouter", "base_url": "https://openrouter.ai/api/v1", "model": os.environ.get("QWENPAW_MODEL", "openai/gpt-4o-mini"), "chat_model": "OpenAIChatModel", "prefix": "sk-or-", }, { "env": "DASHSCOPE_API_KEY", "provider_id": "dashscope", "name": "DashScope", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "model": os.environ.get("QWENPAW_MODEL", "qwen-plus"), "chat_model": "OpenAIChatModel", "prefix": "sk-", }, { "env": "DEEPSEEK_API_KEY", "provider_id": "deepseek", "name": "DeepSeek", "base_url": "https://api.deepseek.com", "model": os.environ.get("QWENPAW_MODEL", "deepseek-chat"), "chat_model": "OpenAIChatModel", "prefix": "sk-", }, { "env": "GEMINI_API_KEY", "provider_id": "gemini", "name": "Google Gemini", "base_url": "https://generativelanguage.googleapis.com", "model": os.environ.get("QWENPAW_MODEL", "gemini-2.5-flash"), "chat_model": "GeminiChatModel", "prefix": "", }, { "env": "GOOGLE_API_KEY", "provider_id": "gemini", "name": "Google Gemini", "base_url": "https://generativelanguage.googleapis.com", "model": os.environ.get("QWENPAW_MODEL", "gemini-2.5-flash"), "chat_model": "GeminiChatModel", "prefix": "", }, ] selected = None for candidate in provider_candidates: api_key = colab_secret_or_env(candidate["env"]) if api_key: selected = {**candidate, "api_key": api_key} break def read_json(path, default): try: if path.exists(): return json.loads(path.read_text(encoding="utf-8")) except Exception: pass return default def write_json(path, data): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") config_path = WORKING_DIR / "config.json" config = read_json(config_path, {}) config.setdefault("agents", {}) config["agents"].setdefault("active_agent", "default") config["agents"].setdefault("agent_order", ["default"]) config["agents"].setdefault("profiles", {}) config["agents"]["profiles"].setdefault("default", {}) config["agents"]["profiles"]["default"].update( { "id": "default", "name": "Colab Research Assistant", "description": "A QwenPaw agent configured for Google Colab tutorials, local files, custom skills, and API testing.", "workspace_dir": str(WORKSPACE_DIR), "enabled": True, } ) config["last_api"] = {"host": "127.0.0.1", "port": PORT} config["show_tool_details"] = True config["user_timezone"] = "Asia/Kolkata" write_json(config_path, config)
We initialize the QwenPaw working directory and prepare the base configuration file for the default agent. We define multiple model-provider options, such as OpenAI, OpenRouter, DashScope, DeepSeek, and Gemini, so the setup can adapt to whichever API key we provide. We then update the QwenPaw configuration with the default agent profile, workspace path, API settings, and timezone.
Copy CodeCopiedUse a different Browser
agent_dir = WORKING_DIR / "agents" / "default" agent_dir.mkdir(parents=True, exist_ok=True) agent_path = agent_dir / "agent.json" agent = read_json(agent_path, {}) agent.update( { "id": "default", "name": "Colab Research Assistant", "description": "Advanced QwenPaw tutorial agent for Colab: file-aware, skill-aware, API-testable, and guarded.", "language": "en", "workspace_dir": str(WORKSPACE_DIR), "enabled": True, "channels": { "console": { "enabled": True } }, "running": { "max_iters": 30, "llm_retry_enabled": True, "stream_output": True }, "security": { "tool_guard": True, "file_guard": True, "skill_scanner": True, "skill_scan_mode": "warn" }, "tool_filter": { "enabled": False, "allow": [], "deny": [] }, "memory": { "enabled": True } } ) if selected: provider_dir = SECRET_DIR / "providers" / "builtin" provider_dir.mkdir(parents=True, exist_ok=True) provider_payload = { "id": selected["provider_id"], "name": selected["name"], "base_url": selected["base_url"], "api_key": selected["api_key"], "chat_model": selected["chat_model"], "models": [], "extra_models": [ { "id": selected["model"], "name": selected["model"], "supports_image": None, "supports_video": None, "supports_multimodal": None, "is_free": False, "max_tokens": int(os.environ.get("QWENPAW_MAX_TOKENS", "2048")), "max_input_length": int(os.environ.get("QWENPAW_MAX_INPUT_LENGTH", "131072")), "generate_kwargs": { "temperature": float(os.environ.get("QWENPAW_TEMPERATURE", "0.2")), "max_tokens": int(os.environ.get("QWENPAW_MAX_TOKENS", "2048")), }, } ], "api_key_prefix": selected["prefix"], "is_local": False, "freeze_url": True, "require_api_key": True, "is_custom": False, "support_model_discovery": False, "support_connection_check": False, "generate_kwargs": { "temperature": float(os.environ.get("QWENPAW_TEMPERATURE", "0.2")), "max_tokens": int(os.environ.get("QWENPAW_MAX_TOKENS", "2048")), }, "custom_headers": {}, "auth_mode": "api_key", "meta": {}, } write_json(provider_dir / f"{selected['provider_id']}.json", provider_payload) write_json( SECRET_DIR / "providers" / "active_model.json", {"provider_id": selected["provider_id"], "model": selected["model"]}, ) agent["active_model"] = {"provider_id": selected["provider_id"], "model": selected["model"]} print(f"Configured model provider: {selected['name']} / {selected['model']}") else: print( "No model key found. The web app will still launch, but chat requires a configured model.\n" "Add one Colab secret or environment variable such as OPENAI_API_KEY, OPENROUTER_API_KEY, " "DASHSCOPE_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY, then rerun." ) write_json(agent_path, agent)
We create the default QwenPaw agent configuration with console access, memory support, streaming output, and guarded tool execution. We automatically configure the selected model provider when a supported API key is available in Colab secrets or environment variables. We save the active model and agent settings so QwenPaw can use the configured provider during chat and API-based interactions.
Copy CodeCopiedUse a different Browser
skill_dir = WORKSPACE_DIR / "skills" / "research_brief" skill_dir.mkdir(parents=True, exist_ok=True) (skill_dir / "SKILL.md").write_text( """--- name: research_brief description: Create rigorous research briefs from user questions, local notes, uploaded files, and available tools. ---
Research Brief Skill
Use this skill when the user asks for research, product analysis, market mapping, technical due diligence, paper analysis, repo analysis, or a decision memo.
Procedure
- Restate the user's objective in one sentence.
- Identify the most important entities, assumptions, and constraints.
- Search available local workspace files first.
- Use tools only when they are relevant and allowed.
- Separate verified facts from inference.
- Produce a compact brief with:
- answer
- evidence
- risks or caveats
- recommended next step
Output Style
Prefer clear sections, short paragraphs, and explicit uncertainty. Do not invent citations, file contents, commands, or results. """, encoding="utf-8", ) demo_dir = WORKSPACE_DIR / "demo_knowledge" demo_dir.mkdir(parents=True, exist_ok=True) (demo_dir / "qwenpaw_colab_notes.md").write_text( f"""# QwenPaw Colab Demo Notes Created: {datetime.now().isoformat(timespec="seconds")} This workspace is prepared by a Google Colab tutorial. The tutorial demonstrates:
- QwenPaw installation and initialization
- provider auto-configuration from Colab secrets or environment variables
- authenticated Console launch
- custom workspace skill creation
- local workspace knowledge files
- streaming REST API calls
- optional public tunnel exposure
Recommended first prompt in the Console: "Read my workspace
[truncated for AI cost control]