Merge pull request #538 from Molecule-AI/devrel/gemini-cli-demo
devrel: gemini-cli runtime adapter demo (closes #534)
This commit is contained in:
commit
fbe48ba4f0
15
docs/marketing/devrel/gemini-cli-demo/Makefile
Normal file
15
docs/marketing/devrel/gemini-cli-demo/Makefile
Normal file
@ -0,0 +1,15 @@
|
||||
.PHONY: run deps check-env
|
||||
|
||||
## Install Python dependency
|
||||
deps:
|
||||
pip install httpx
|
||||
|
||||
## Verify required env vars are set before running
|
||||
check-env:
|
||||
@test -n "$(PLATFORM_URL)" || (echo "Error: PLATFORM_URL is not set" && exit 1)
|
||||
@test -n "$(PLATFORM_TOKEN)" || (echo "Error: PLATFORM_TOKEN is not set" && exit 1)
|
||||
@test -n "$(GEMINI_API_KEY)" || (echo "Error: GEMINI_API_KEY is not set" && exit 1)
|
||||
|
||||
## Run the demo end-to-end
|
||||
run: deps check-env
|
||||
python demo.py
|
||||
176
docs/marketing/devrel/gemini-cli-demo/README.md
Normal file
176
docs/marketing/devrel/gemini-cli-demo/README.md
Normal file
@ -0,0 +1,176 @@
|
||||
# Gemini CLI Runtime Adapter — Live Demo
|
||||
|
||||
> **Feature:** [`feat(adapters): add gemini-cli runtime adapter`](https://github.com/Molecule-AI/molecule-core/pull/379)
|
||||
> **Adapter path:** `workspace-template/adapters/gemini_cli/`
|
||||
> **Runtime key:** `gemini-cli`
|
||||
|
||||
This demo provisions a Gemini CLI workspace on Molecule AI, sends it a task via
|
||||
the A2A proxy, and prints the result — all in about 60 seconds.
|
||||
|
||||
---
|
||||
|
||||
## What you'll need
|
||||
|
||||
| Requirement | Where to get it |
|
||||
|-------------|----------------|
|
||||
| Running Molecule AI platform | See [Quickstart](../../docs/quickstart.md) |
|
||||
| Admin bearer token | Printed on first `go run ./cmd/server` startup |
|
||||
| `GEMINI_API_KEY` | [Google AI Studio → Get API key](https://aistudio.google.com/apikey) |
|
||||
| Python ≥ 3.11 + pip | `python --version` |
|
||||
| `@google/gemini-cli` Docker image built | `bash workspace-template/build-all.sh gemini-cli` |
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step walkthrough
|
||||
|
||||
### 1 — Build the adapter image (one-time)
|
||||
|
||||
```bash
|
||||
# From the repo root
|
||||
bash workspace-template/build-all.sh gemini-cli
|
||||
```
|
||||
|
||||
Expected output: `Successfully tagged workspace-template:gemini-cli`
|
||||
|
||||
This installs `@google/gemini-cli@0.38.1` globally inside the container and
|
||||
wires the A2A MCP server into `~/.gemini/settings.json` at boot. The adapter
|
||||
seeds `GEMINI.md` from `system-prompt.md` so the agent has role context on
|
||||
first message.
|
||||
|
||||
---
|
||||
|
||||
### 2 — Set environment variables
|
||||
|
||||
```bash
|
||||
export PLATFORM_URL=http://localhost:8080 # your running platform
|
||||
export PLATFORM_TOKEN=<admin-bearer-token> # printed at startup
|
||||
export GEMINI_API_KEY=<your-api-key> # NEVER hardcode this
|
||||
```
|
||||
|
||||
The demo script reads all credentials from env vars — no secrets in source.
|
||||
|
||||
---
|
||||
|
||||
### 3 — Run
|
||||
|
||||
```bash
|
||||
make run
|
||||
# or: pip install httpx && python demo.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected output
|
||||
|
||||
```
|
||||
[1] Creating gemini-cli workspace...
|
||||
created id=a1b2c3d4-5678-...
|
||||
|
||||
[2] Storing GEMINI_API_KEY as workspace secret (value never logged)...
|
||||
secret stored
|
||||
|
||||
[3] Waiting for workspace to come online (up to 90 s)...
|
||||
online in ~18 s
|
||||
|
||||
[4] Sending task via A2A proxy...
|
||||
Task: "List the three biggest advantages of Google Gemini 2.5 Pro ..."
|
||||
|
||||
[5] Gemini CLI agent reply:
|
||||
|
||||
1. Gemini 2.5 Pro's one-million-token context window lets it ingest entire
|
||||
codebases in a single pass, eliminating the repeated context-loading
|
||||
overhead GPT-4o requires.
|
||||
2. Its native multimodal input natively processes screenshots and diagrams
|
||||
alongside code, so UI-driven debugging tasks need no preprocessing step.
|
||||
3. Google's function-calling latency benchmarks show lower P99 for
|
||||
tool-call round-trips, which compounds in ReAct loops across many steps.
|
||||
|
||||
[6] Deleting demo workspace...
|
||||
workspace deleted
|
||||
|
||||
Demo complete.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How it works — under the hood
|
||||
|
||||
```
|
||||
demo.py
|
||||
│
|
||||
├─ POST /workspaces → platform creates Docker container
|
||||
│ runtime: gemini-cli adapter.setup() writes ~/.gemini/settings.json
|
||||
│ seeds GEMINI.md from system-prompt.md
|
||||
│
|
||||
├─ PUT /workspaces/:id/secrets → GEMINI_API_KEY stored AES-256-GCM
|
||||
│
|
||||
├─ GET /workspaces/:id (poll) → waits for status=="online"
|
||||
│ (workspace registers via POST /registry/register)
|
||||
│
|
||||
├─ POST /workspaces/:id/a2a → JSON-RPC 2.0 method: message/send
|
||||
│ platform proxies to gemini CLI subprocess
|
||||
│ CLI runs: gemini --yolo --model gemini-2.5-flash -p "<task>"
|
||||
│ MCP tools (delegate_task, commit_memory, …) available via settings.json
|
||||
│
|
||||
└─ DELETE /workspaces/:id → container removed
|
||||
```
|
||||
|
||||
### Key adapter decisions (from PR #379)
|
||||
|
||||
| Decision | Why |
|
||||
|----------|-----|
|
||||
| `~/.gemini/settings.json` for MCP | Gemini CLI ignores `--mcp-config`; adapter merges A2A server entry on `setup()`, preserving user's existing MCP tools |
|
||||
| `GEMINI.md` as memory file | Equivalent of `CLAUDE.md` for Claude Code; seeded from `system-prompt.md` on first boot so agents start with role context |
|
||||
| `--yolo` flag | Non-interactive mode — auto-approves all tool calls, required for headless subprocess execution |
|
||||
| `gemini-2.5-flash` for demo | Faster boot; switch to `gemini-2.5-pro` for production workspaces needing deeper reasoning |
|
||||
|
||||
---
|
||||
|
||||
## Swap in a different model
|
||||
|
||||
```bash
|
||||
# In demo.py, change runtime_config.model:
|
||||
"model": "gemini-2.5-pro", # full reasoning
|
||||
"model": "gemini-2.0-flash", # fastest, cheapest
|
||||
```
|
||||
|
||||
Or set it per-workspace via the Molecule AI canvas → Config → Runtime.
|
||||
|
||||
---
|
||||
|
||||
## Multi-provider example
|
||||
|
||||
Once you have a `gemini-cli` workspace running alongside a `claude-code` workspace,
|
||||
you can delegate tasks between them transparently — the A2A protocol is runtime-agnostic:
|
||||
|
||||
```python
|
||||
# From your orchestrator workspace (claude-code, hermes, etc.)
|
||||
result = delegate_task(
|
||||
workspace_id="<gemini-cli-workspace-id>",
|
||||
task="Summarise the attached diff and suggest three test cases.",
|
||||
)
|
||||
```
|
||||
|
||||
No code changes needed. The orchestrator doesn't know (or care) which model
|
||||
is running on the other side.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
|---------|-----|
|
||||
| Workspace stuck in `provisioning` | Check `docker images` for `workspace-template:gemini-cli`; re-run `build-all.sh gemini-cli` if missing |
|
||||
| `failed` status immediately | Check platform logs: `GEMINI_API_KEY` missing or `npm install -g @google/gemini-cli` failed during image build |
|
||||
| A2A call times out | `gemini-cli` cold-start on first task can take 15–20 s; increase `timeout=120` in demo.py if needed |
|
||||
| `code 422` on workspace create | Platform requires `runtime: "gemini-cli"` to be in `RUNTIME_PRESETS`; confirm you're on main after PR #379 |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [PR #379 — gemini-cli runtime adapter](https://github.com/Molecule-AI/molecule-core/pull/379)
|
||||
- [Tutorial: Running a Gemini CLI Workspace](../../docs/tutorials/gemini-cli-runtime.md) *(PR #509)*
|
||||
- [Adapter source](../../workspace-template/adapters/gemini_cli/adapter.py)
|
||||
- [CLI executor preset](../../workspace-template/cli_executor.py)
|
||||
- [A2A proxy API reference](../../docs/api-reference.md#a2a-proxy)
|
||||
164
docs/marketing/devrel/gemini-cli-demo/demo.py
Normal file
164
docs/marketing/devrel/gemini-cli-demo/demo.py
Normal file
@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Gemini CLI runtime adapter — live demo
|
||||
Molecule AI | feat(adapters): add gemini-cli runtime adapter (#379)
|
||||
|
||||
Spins up a gemini-cli workspace, sends a task via the A2A proxy,
|
||||
prints the reply, then tears down the workspace.
|
||||
|
||||
Usage:
|
||||
pip install httpx
|
||||
export PLATFORM_URL=http://localhost:8080
|
||||
export PLATFORM_TOKEN=<admin-bearer-token>
|
||||
export GEMINI_API_KEY=<your-google-ai-studio-key>
|
||||
python demo.py
|
||||
|
||||
No API keys are ever hardcoded or logged.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
print("Missing dependency: pip install httpx")
|
||||
sys.exit(1)
|
||||
|
||||
# ── Config (all from environment — no hardcoded values) ──────────────────────
|
||||
PLATFORM_URL = os.environ.get("PLATFORM_URL", "").rstrip("/")
|
||||
PLATFORM_TOKEN = os.environ.get("PLATFORM_TOKEN", "")
|
||||
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
|
||||
|
||||
MISSING = [k for k, v in {
|
||||
"PLATFORM_URL": PLATFORM_URL,
|
||||
"PLATFORM_TOKEN": PLATFORM_TOKEN,
|
||||
"GEMINI_API_KEY": GEMINI_API_KEY,
|
||||
}.items() if not v]
|
||||
if MISSING:
|
||||
print(f"Missing required env vars: {', '.join(MISSING)}")
|
||||
sys.exit(1)
|
||||
|
||||
HEADERS = {
|
||||
"Authorization": f"Bearer {PLATFORM_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
TASK = (
|
||||
"List the three biggest advantages of Google Gemini 2.5 Pro "
|
||||
"over GPT-4o for agentic coding tasks. One sentence each."
|
||||
)
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def step(n: int, msg: str) -> None:
|
||||
print(f"\n\033[1;34m[{n}]\033[0m {msg}")
|
||||
|
||||
|
||||
def die(msg: str) -> None:
|
||||
print(f"\n\033[1;31m✗\033[0m {msg}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def api(method: str, path: str, **kwargs) -> dict:
|
||||
"""Make an authenticated request; exit on non-2xx."""
|
||||
url = f"{PLATFORM_URL}{path}"
|
||||
with httpx.Client(timeout=kwargs.pop("timeout", 30)) as client:
|
||||
resp = getattr(client, method)(url, headers=HEADERS, **kwargs)
|
||||
if resp.status_code not in (200, 201, 204):
|
||||
die(f"HTTP {resp.status_code} {method.upper()} {path}: {resp.text[:300]}")
|
||||
return resp.json() if resp.content else {}
|
||||
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> None:
|
||||
workspace_id: str | None = None
|
||||
|
||||
try:
|
||||
# 1. Create the gemini-cli workspace
|
||||
step(1, "Creating gemini-cli workspace...")
|
||||
ws = api("post", "/workspaces", json={
|
||||
"name": "gemini-cli-demo",
|
||||
"role": "Molecule AI gemini-cli adapter demo",
|
||||
"runtime": "gemini-cli",
|
||||
"runtime_config": {
|
||||
"model": "gemini-2.5-flash", # flash: faster boot for demo purposes
|
||||
"timeout": 0,
|
||||
},
|
||||
"tier": 2, # 2 GB / 2 vCPU
|
||||
})
|
||||
workspace_id = ws["id"]
|
||||
print(f" created id={workspace_id}")
|
||||
|
||||
# 2. Inject GEMINI_API_KEY as a workspace-scoped secret
|
||||
step(2, "Storing GEMINI_API_KEY as workspace secret (value never logged)...")
|
||||
api("put", f"/workspaces/{workspace_id}/secrets",
|
||||
json={"key": "GEMINI_API_KEY", "value": GEMINI_API_KEY})
|
||||
print(" secret stored")
|
||||
|
||||
# 3. Wait for the workspace container to boot and register
|
||||
step(3, "Waiting for workspace to come online (up to 90 s)...")
|
||||
for attempt in range(30):
|
||||
ws = api("get", f"/workspaces/{workspace_id}", timeout=10)
|
||||
status = ws.get("status", "unknown")
|
||||
print(f" {status:12s} ({attempt + 1}/30)", end="\r", flush=True)
|
||||
if status == "online":
|
||||
print(f"\n online in ~{attempt * 3} s")
|
||||
break
|
||||
if status in ("failed", "error"):
|
||||
die(f"workspace entered error state: {status}")
|
||||
time.sleep(3)
|
||||
else:
|
||||
die("timed out waiting for 'online' status")
|
||||
|
||||
# 4. Send a task via the A2A proxy (JSON-RPC 2.0 over HTTP)
|
||||
step(4, "Sending task via A2A proxy...")
|
||||
print(f' Task: "{TASK}"')
|
||||
result = api(
|
||||
"post",
|
||||
f"/workspaces/{workspace_id}/a2a",
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"id": str(uuid.uuid4()),
|
||||
"method": "message/send",
|
||||
"params": {
|
||||
"message": {
|
||||
"role": "user",
|
||||
"parts": [{"kind": "text", "text": TASK}],
|
||||
}
|
||||
},
|
||||
},
|
||||
timeout=120, # agent may take a moment to reason
|
||||
)
|
||||
|
||||
# 5. Extract the text reply from the A2A response envelope
|
||||
step(5, "Gemini CLI agent reply:")
|
||||
try:
|
||||
parts = result["result"]["status"]["message"]["parts"]
|
||||
reply = "\n".join(
|
||||
p["text"] for p in parts if p.get("kind") == "text"
|
||||
)
|
||||
except (KeyError, TypeError):
|
||||
reply = str(result)
|
||||
|
||||
print()
|
||||
for line in reply.splitlines():
|
||||
print(f" {line}")
|
||||
print()
|
||||
|
||||
finally:
|
||||
# 6. Always clean up — even if an earlier step failed
|
||||
if workspace_id:
|
||||
step(6, "Deleting demo workspace...")
|
||||
api("delete", f"/workspaces/{workspace_id}", timeout=15)
|
||||
print(" workspace deleted")
|
||||
|
||||
print("\033[1;32mDemo complete.\033[0m\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user