diff --git a/.github/workflows/test-obol-agent-ag-ui.yml b/.github/workflows/test-obol-agent-ag-ui.yml new file mode 100644 index 00000000..d48765fd --- /dev/null +++ b/.github/workflows/test-obol-agent-ag-ui.yml @@ -0,0 +1,71 @@ +name: Test Obol Agent AG-UI + +on: + push: + branches: [ main, feature/* ] + paths: + - 'obol-adk/obol-agent-ag-ui/**' + - '.github/workflows/test-obol-agent-ag-ui.yml' + pull_request: + branches: [ main ] + paths: + - 'obol-adk/obol-agent-ag-ui/**' + - '.github/workflows/test-obol-agent-ag-ui.yml' + +jobs: + test-agent-endpoint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + cd obol-adk/obol-agent-ag-ui + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install requests + + - name: Start agent in background + env: + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + run: | + cd obol-adk/obol-agent-ag-ui + # Create minimal .env + echo "FILESYSTEM_MCP_PATHS=" > .env + echo "GOOGLE_API_KEY=${GOOGLE_API_KEY}" >> .env + # Start agent in background + nohup python agent.py > agent.log 2>&1 & + echo $! > agent.pid + # Wait for agent to start + sleep 10 + + - name: Run health check + run: | + # Check if agent is responding + curl -f http://localhost:8000/health || (cat obol-adk/obol-agent-ag-ui/agent.log && exit 1) + + - name: Run endpoint tests + run: | + cd obol-adk/obol-agent-ag-ui + python test_agent.py || (cat agent.log && exit 1) + + - name: Stop agent + if: always() + run: | + if [ -f obol-adk/obol-agent-ag-ui/agent.pid ]; then + kill $(cat obol-adk/obol-agent-ag-ui/agent.pid) || true + fi + + - name: Upload agent logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: agent-logs + path: obol-adk/obol-agent-ag-ui/agent.log + diff --git a/.gitignore b/.gitignore index b2fbe33a..62353c74 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,33 @@ build/ tmp/ temp/ .tmp/ +__pycache__/ + +adk-samples/ +obol-adk/docs/* +obol-adk/obol-gitbook/ + +# SECURITY Prevent sensitive data leaks +# ======================================== + +# Cluster test data and keys +**/canary-*/ +**/node[0-9]*/ +**/cluster-lock.json +**/validator_keys/ +**/keystore-*.json +**/keystore-*.txt +**/charon-enr-private-key + +# Dependencies +**/node_modules/ + +# Local test files +test-*.yaml +demo-*.yaml +*.log +*.pid + +# Local LLMs +llms-full.txt +CLAUDE.md diff --git a/obol-adk/.env.example b/obol-adk/.env.example new file mode 100644 index 00000000..ccb616c0 --- /dev/null +++ b/obol-adk/.env.example @@ -0,0 +1,36 @@ +# Obol ADK Configuration +# This file is used by obol-agent-ag-ui/agent.py + +# Google API Configuration +# Get your API key from: https://aistudio.google.com/app/apikey +GOOGLE_GENAI_USE_VERTEXAI=FALSE +GOOGLE_API_KEY=your-api-key-here + +# Prometheus MCP Server Configuration (optional) +# PROMETHEUS_URL=http://prometheus-kube-prometheus-prometheus.monitoring.svc.cluster.local:9090 + +# Filesystem MCP Server Paths (comma-separated list) +# The 'make setup' command will automatically configure this to point to obol-gitbook +# Each path will be accessible via the filesystem MCP server for documentation access +FILESYSTEM_MCP_PATHS= + +## Development Workflow +# +# Quick Start: +# make dev # Full setup (venv + gitbook + env) and start agent +# +# Individual Commands: +# make install # Create venv and install Python dependencies +# make setup # Clone/update obol-gitbook and configure this .env +# make start # Start the obol-agent-ag-ui agent +# make test # Run agent tests +# make clean # Remove obol-gitbook directory +# +# The 'make install' command will: +# 1. Create a Python virtual environment at ./.venv/ +# 2. Install dependencies from requirements.txt +# +# The 'make setup' command will: +# 1. Clone https://github.com/ObolNetwork/obol-gitbook.git to ./obol-gitbook/ +# 2. Update FILESYSTEM_MCP_PATHS in this .env file +# 3. Preserve your existing GOOGLE_API_KEY diff --git a/obol-adk/Makefile b/obol-adk/Makefile new file mode 100644 index 00000000..8889541a --- /dev/null +++ b/obol-adk/Makefile @@ -0,0 +1,143 @@ +.PHONY: help install setup start start-web dev clean test ci deploy + +# Use absolute path for venv to work from subdirectories +VENV := $(CURDIR)/.venv +PYTHON := $(VENV)/bin/python +PIP := $(VENV)/bin/pip +ADK := $(VENV)/bin/adk +AGENT_DIR := $(CURDIR)/obol-agent-ag-ui +WEB_AGENT_DIR := $(CURDIR)/obol-agent-web + +# Default target +help: + @echo "Obol ADK - Development Commands" + @echo "" + @echo "Usage:" + @echo " make dev - Full setup (venv + gitbook + env) and start agent" + @echo " make install - Create venv and install Python dependencies" + @echo " make setup - Clone/update obol-gitbook and configure .env" + @echo " make start - Start the obol-agent-ag-ui agent (FastAPI backend)" + @echo " make start-web - Start the obol-agent-web agent (ADK Web UI)" + @echo " make test - Run agent unit tests" + @echo " make ci - Run full CI workflow locally (simulates GitHub Actions)" + @echo " make deploy - Deploy obol-agent-ag-ui to Google Cloud Run" + @echo " make clean - Remove obol-gitbook directory" + @echo "" + +# Create virtual environment and install dependencies +install: + @echo "Setting up Python virtual environment..." + @if [ ! -d "$(VENV)" ]; then \ + python3 -m venv $(VENV); \ + echo "✓ Virtual environment created"; \ + else \ + echo "✓ Virtual environment already exists"; \ + fi + @echo "Installing dependencies..." + @$(PIP) install --upgrade pip + @$(PIP) install -r requirements.txt + @echo "✓ Dependencies installed" + +# Setup development environment (clone gitbook, configure .env) +setup: + @echo "Setting up development environment..." + @$(PYTHON) scripts/setup_dev.py + +# Start the agent (assumes install + setup are done) +start: + @if [ ! -d "$(VENV)" ]; then \ + echo "✗ Virtual environment not found. Run 'make install' first."; \ + exit 1; \ + fi + @echo "Starting Obol Agent AG-UI (FastAPI backend)..." + @echo "Available at: http://localhost:8000/" + @cd obol-agent-ag-ui && $(PYTHON) agent.py + +# Start the web agent with ADK Web UI +start-web: + @if [ ! -d "$(VENV)" ]; then \ + echo "✗ Virtual environment not found. Run 'make install' first."; \ + exit 1; \ + fi + @echo "Starting Obol Agent Web UI..." + @echo "This will launch the ADK Web interface" + @echo "" + @echo "Important: Make sure to select 'obol-agent-web' from the dropdown in the web UI" + @echo "" + @$(ADK) web + +# Dev workflow: install + setup + start +dev: install setup + @echo "" + @echo "Starting agent..." + @cd obol-agent-ag-ui && $(PYTHON) agent.py + +# Run tests +test: + @if [ ! -d "$(VENV)" ]; then \ + echo "✗ Virtual environment not found. Run 'make install' first."; \ + exit 1; \ + fi + @echo "Running agent tests..." + @cd obol-agent-ag-ui && $(PYTHON) test_agent.py + +# Run CI workflow locally (simulates GitHub Actions) +ci: + @if [ ! -d "$(VENV)" ]; then \ + echo "✗ Virtual environment not found. Run 'make install' first."; \ + exit 1; \ + fi + @echo "===================================================================" + @echo "Running CI workflow (simulates GitHub Actions)" + @echo "===================================================================" + @echo "" + @echo "Step 1: Installing test dependencies..." + @$(PIP) install requests > /dev/null 2>&1 + @echo "✓ Dependencies installed" + @echo "" + @echo "Step 2: Creating minimal .env..." + @cd $(AGENT_DIR) && echo "FILESYSTEM_MCP_PATHS=" > .env + @cd $(AGENT_DIR) && echo "GOOGLE_API_KEY=$${GOOGLE_API_KEY}" >> .env + @echo "✓ .env created" + @echo "" + @echo "Step 3: Starting agent in background..." + @cd $(AGENT_DIR) && (nohup $(PYTHON) agent.py > agent.log 2>&1 & echo $$! > agent.pid) + @sleep 1 + @echo "✓ Agent started (PID: $$(cat $(AGENT_DIR)/agent.pid 2>/dev/null || echo 'unknown'))" + @echo "" + @echo "Step 4: Waiting for agent to start (10s)..." + @sleep 10 + @echo "✓ Ready" + @echo "" + @echo "Step 5: Running health check..." + @curl -f http://localhost:8000/health > /dev/null 2>&1 || (echo "✗ Health check failed" && cat $(AGENT_DIR)/agent.log && kill $$(cat $(AGENT_DIR)/agent.pid) 2>/dev/null || true && exit 1) + @echo "✓ Health check passed" + @echo "" + @echo "Step 6: Running endpoint tests..." + @cd $(AGENT_DIR) && $(PYTHON) test_agent.py || (echo "✗ Tests failed" && cat agent.log && kill $$(cat agent.pid) 2>/dev/null || true && exit 1) + @echo "✓ Tests passed" + @echo "" + @echo "Step 7: Stopping agent..." + @kill $$(cat $(AGENT_DIR)/agent.pid) 2>/dev/null || true + @rm -f $(AGENT_DIR)/agent.pid $(AGENT_DIR)/agent.log + @echo "✓ Agent stopped" + @echo "" + @echo "===================================================================" + @echo "✓ CI workflow completed successfully!" + @echo "===================================================================" + +# Deploy to Google Cloud Run +deploy: + @echo "Deploying obol-agent-ag-ui to Google Cloud Run..." + @echo "Region: us-east4" + @echo "Project: prj-d-playgrounds-f0cb" + @cd $(AGENT_DIR) && gcloud builds submit \ + --config=cloudbuild.yaml \ + --region=us-east4 \ + --substitutions=SHORT_SHA=$$(git rev-parse --short HEAD) + +# Clean up gitbook directory +clean: + @echo "Cleaning up obol-gitbook..." + @rm -rf obol-gitbook + @echo "✓ Cleaned" diff --git a/obol-adk/README.md b/obol-adk/README.md new file mode 100644 index 00000000..fa4bfad2 --- /dev/null +++ b/obol-adk/README.md @@ -0,0 +1,229 @@ +# Obol Agents + +This directory contains examples of integrating Google's Agent Development Kit (ADK) with various Model Context Protocol (MCP) servers related to Obol Stack. + +> **Note:** Docker configurations have been moved to individual agent directories. See [DOCKER_NOTE.md](DOCKER_NOTE.md) for details. + +## Quick Start + +For the AG-UI agent (recommended), use the Makefile workflow: + +```bash +# One command to set up everything and start the agent +make dev + +# Or run individual steps: +make install # Create venv and install dependencies +make setup # Clone obol-gitbook and configure .env +make start # Start the agent +make test # Run tests +``` + +See [Development Workflow](#development-workflow) below for details. + +## Prerequisites + +1. **Python Environment:** Python 3.12+ is required for the AG-UI agent. +2. **Google API Key:** Required for the Gemini model. Get your key from [Google AI Studio](https://aistudio.google.com/app/apikey). +3. **Environment Variables:** Create a `.env` file in the `obol-adk` directory: + ```bash + GOOGLE_API_KEY=your_google_api_key_here + FILESYSTEM_MCP_PATHS=/path/to/obol-gitbook # Auto-configured by 'make setup' + ``` +4. **MCP Servers:** The agents use published MCP packages via `uvx` and `npx`: + - `obol-mcp` - Obol cluster operations + - `@modelcontextprotocol/server-filesystem` - Documentation access + - `mcp-server-kubernetes` - Kubernetes operations (optional) + +## Running the Agents + +### 1. Command-Line Agent (`obol-agent/agent.py`) + +This agent connects to Obol, Kubernetes, and Foundry MCP servers and runs directly in the terminal. + +**To run:** + +1. Navigate to the `obol-agent` directory: + ```bash + cd /Users/bussyjd/Development/Obol_Workbench/obol-stack/obol-adk/obol-agent + ``` +2. Ensure your `.env` file is present in this directory. +3. Run the agent script: + ```bash + python3 agent.py + ``` + The script will prompt for a user query after connecting to the servers. + +### 2. Web UI Agent (`obol-agent-web/agent.py`) + +This agent is designed to be run using the ADK Web UI. It connects to Obol, Kubernetes, and Filesystem MCP servers. + +**To run (recommended):** + +```bash +# From the obol-adk directory +make start-web +``` + +This will launch the ADK Web interface. Make sure to select `obol_agent` from the dropdown in the web UI. + +**Manual alternative:** + +1. Navigate to the `obol-adk` root directory: + ```bash + cd /Users/bussyjd/Development/Obol_Workbench/obol-stack/obol-adk + ``` +2. Ensure your `.env` file is configured (see `.env.example`) +3. Start the ADK Web server: + ```bash + cd obol-agent-web && adk web + ``` +4. Open your web browser and navigate to the URL provided +5. Select `obol_agent` from the dropdown list in the ADK Web UI +6. Interact with the agent through the chat interface + + +### 3. AG-UI Backend Agent (`obol-agent-ag-ui/agent.py`) - Recommended + +This agent provides an AG-UI backend for interacting with Obol Stack through a modern web interface. It includes access to Obol documentation via the filesystem MCP server. + +**To run locally (recommended):** + +Use the Makefile workflow from the `obol-adk` directory: + +```bash +# First time setup - creates venv, installs deps, clones docs, configures .env +make dev + +# Subsequent runs +make start +``` + +The AG-UI backend will be available at `http://localhost:8000/` + +**Manual setup (alternative):** + +1. Navigate to the `obol-adk` directory: + ```bash + cd /Users/bussyjd/Development/Obol_Workbench/obol-stack/obol-adk + ``` +2. Create and activate virtual environment: + ```bash + python3 -m venv .venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + ``` +3. Install dependencies: + ```bash + pip install -r requirements.txt + ``` +4. Clone documentation: + ```bash + git clone https://github.com/ObolNetwork/obol-gitbook.git + ``` +5. Create `.env` file with required variables (see `.env.example`) +6. Run the agent: + ```bash + cd obol-agent-ag-ui && python agent.py + ``` + +**To run with Docker:** + +1. Navigate to the `obol-agent-ag-ui` directory +2. Build and run with Docker Compose: + ```bash + docker-compose up -d + ``` + Or build manually: + ```bash + docker build -t obol-agent-ag-ui:latest . + docker run -d -p 8000:8000 --env-file ../.env obol-agent-ag-ui:latest + ``` + +See the [obol-agent-ag-ui README](obol-agent-ag-ui/README.md) for detailed configuration options. + +## Development Workflow + +The `obol-adk` directory includes a Makefile for convenient development workflows: + +### Available Commands + +```bash +make help # Show all available commands +make dev # Full setup and start (one command for first-time setup) +make install # Create virtual environment and install dependencies +make setup # Clone/update obol-gitbook and configure .env +make start # Start the obol-agent-ag-ui agent (FastAPI backend) +make start-web # Start the obol-agent-web agent (ADK Web UI) +make test # Run agent unit tests +make ci # Run full CI workflow locally (simulates GitHub Actions) +make clean # Remove obol-gitbook directory +``` + +### How It Works + +1. **`make install`** - Sets up Python environment + - Creates `.venv/` virtual environment (if it doesn't exist) + - Installs all dependencies from `requirements.txt` + - Upgrades pip to latest version + +2. **`make setup`** - Configures project-specific settings + - Clones [obol-gitbook](https://github.com/ObolNetwork/obol-gitbook) to `./obol-gitbook/` + - Updates `FILESYSTEM_MCP_PATHS` in `.env` to point to the docs + - Preserves your existing `GOOGLE_API_KEY` + +3. **`make start`** - Runs the agent + - Verifies virtual environment exists + - Starts the AG-UI backend on `http://localhost:8000` + +4. **`make dev`** - Combines `install` + `setup` + `start` + - Perfect for first-time setup + - One command to go from zero to running agent + +5. **`make test`** - Runs unit tests + - Quick test suite for development + - Tests agent health and basic functionality + +6. **`make ci`** - Full CI workflow simulation + - Simulates the exact GitHub Actions workflow + - Runs agent in background, performs health checks, runs full test suite + - Perfect for validating changes before pushing + - Requires `GOOGLE_API_KEY` in `.env` + +### First Time Setup + +```bash +# 1. Clone the repository (if you haven't already) +git clone https://github.com/ObolNetwork/obol-stack.git +cd obol-stack/obol-adk + +# 2. Create .env file with your API key +cp .env.example .env +# Edit .env and add your GOOGLE_API_KEY + +# 3. Run dev workflow +make dev +``` + +The agent will be available at `http://localhost:8000/` with access to: +- Obol cluster operations via `obol-mcp` +- Obol documentation via `@modelcontextprotocol/server-filesystem` +- Kubernetes operations via `mcp-server-kubernetes` (if available) + +## Improvements + +- Awaiting MCP public package for Foundry MCP server https://github.com/PraneshASP/foundry-mcp-server?tab=readme-ov-file#setup-using-npm-package + +## Docker Images for MCP Servers + +Dockerfiles that package the MCP servers used by `obol-agent-web` are located in the `dockerfiles/` directory: + +- **Dockerfile.filesystem** – packages the Filesystem MCP server. +- **Dockerfile.obol** – packages the Obol MCP server. +- **Dockerfile.kubernetes** – packages the Kubernetes MCP server. +- **Dockerfile.foundry** – packages the Foundry MCP server. + +These images can be built and pushed to your own registry, for example: + +```bash +docker build -f dockerfiles/Dockerfile.obol -t /obol-mcp:latest . +``` diff --git a/obol-adk/main.py b/obol-adk/main.py new file mode 100644 index 00000000..0c269e25 --- /dev/null +++ b/obol-adk/main.py @@ -0,0 +1,33 @@ +import os + +import uvicorn +from fastapi import FastAPI +from google.adk.cli.fast_api import get_fast_api_app + +# Get the directory where main.py is located +AGENT_DIR = os.path.dirname(os.path.abspath(__file__)) +# Example session DB URL (e.g., SQLite) +SESSION_DB_URL = "sqlite:///./sessions.db" +# Example allowed origins for CORS +ALLOWED_ORIGINS = ["http://localhost", "http://localhost:8080", "*"] +# Set web=True if you intend to serve a web interface, False otherwise +SERVE_WEB_INTERFACE = True + +# Call the function to get the FastAPI app instance +# Ensure the agent directory name ('capital_agent') matches your agent folder +app: FastAPI = get_fast_api_app( + agent_dir=AGENT_DIR, + session_db_url=SESSION_DB_URL, + allow_origins=ALLOWED_ORIGINS, + web=SERVE_WEB_INTERFACE, +) + +# You can add more FastAPI routes or configurations below if needed +# Example: +# @app.get("/hello") +# async def read_root(): +# return {"Hello": "World"} + +if __name__ == "__main__": + # Use the PORT environment variable provided by Cloud Run, defaulting to 8080 + uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) \ No newline at end of file diff --git a/obol-adk/obol-agent-ag-ui/.dockerignore b/obol-adk/obol-agent-ag-ui/.dockerignore new file mode 100644 index 00000000..76be44ff --- /dev/null +++ b/obol-adk/obol-agent-ag-ui/.dockerignore @@ -0,0 +1,46 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +.venv +pip-log.txt +pip-delete-this-directory.txt +.pytest_cache/ +*.egg-info/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Git +.git/ +.gitignore + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Documentation +*.md +docs/ + +# Testing +test/ +tests/ +coverage/ +.coverage +htmlcov/ \ No newline at end of file diff --git a/obol-adk/obol-agent-ag-ui/.env.example b/obol-adk/obol-agent-ag-ui/.env.example new file mode 100644 index 00000000..2f0b8b6b --- /dev/null +++ b/obol-adk/obol-agent-ag-ui/.env.example @@ -0,0 +1,24 @@ +# Obol Agent AG-UI Configuration + +# Server Configuration +PORT=8000 +LOG_REQUESTS=false + +# LLM Configuration +LLM_MODEL=gemini-2.0-flash + +# MCP Servers (disabled by default in containers) +ENABLE_MCP_SERVERS=false + +# MCP Server Paths (used only when ENABLE_MCP_SERVERS=true) +# These paths would need to be mounted into the container +OBOL_MCP_PATH=/opt/obol-mcp +K8S_MCP_PATH=/opt/kubernetes-mcp-server +FOUNDRY_MCP_PATH=/opt/foundry-mcp-server + +# Google API Credentials (if needed) +# GOOGLE_APPLICATION_CREDENTIALS=/app/credentials.json + +# Filesystem MCP Server Paths (comma-separated list) +# Each path will be accessible via the filesystem MCP server +FILESYSTEM_MCP_PATHS=/path/to/obol-gitbook,/path/to/other-docs \ No newline at end of file diff --git a/obol-adk/obol-agent-ag-ui/Dockerfile b/obol-adk/obol-agent-ag-ui/Dockerfile new file mode 100644 index 00000000..d0e54d4f --- /dev/null +++ b/obol-adk/obol-agent-ag-ui/Dockerfile @@ -0,0 +1,44 @@ +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +WORKDIR /app + +# Install Node.js for npx (filesystem MCP server) and git for cloning docs +RUN apt-get update && apt-get install -y --no-install-recommends \ + nodejs \ + npm \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Create non-root user +RUN adduser --disabled-password --gecos "" obol-agent && \ + chown -R obol-agent:obol-agent /app + +# Clone obol-gitbook documentation +RUN git clone --depth 1 https://github.com/ObolNetwork/obol-gitbook.git /app/obol-gitbook + +# Copy application code +COPY . . + +# Set ownership for non-root user +RUN chown -R obol-agent:obol-agent /app + +# Switch to non-root user +USER obol-agent + +# Set environment variables +ENV PATH="/home/obol-agent/.local/bin:$PATH" +ENV PORT=8000 +ENV PYTHONUNBUFFERED=1 +ENV FILESYSTEM_MCP_PATHS=/app/obol-gitbook +ENV PUBLIC_MODE=false + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:${PORT}/health')" || exit 1 + +# Run the AG-UI agent with published MCP packages +CMD ["python", "agent.py"] \ No newline at end of file diff --git a/obol-adk/obol-agent-ag-ui/README.md b/obol-adk/obol-agent-ag-ui/README.md new file mode 100644 index 00000000..8fa197fb --- /dev/null +++ b/obol-adk/obol-agent-ag-ui/README.md @@ -0,0 +1,87 @@ +# Obol Agent AG-UI Backend + +AG-UI backend for Obol Agent with MCP tools integration. + +## Quick Start with Docker + +### Using Docker Compose (Recommended) + +```bash +# Build and run +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop +docker-compose down +``` + +The service will be available at `http://localhost:8000`. + +### Using Docker Directly + +```bash +# Build the image +docker build -t obol-agent-ag-ui:latest . + +# Run the container +docker run -d \ + --name obol-agent-ag-ui \ + -p 8000:8000 \ + -e LLM_MODEL=gemini-2.0-flash \ + obol-agent-ag-ui:latest +``` + +## Configuration + +Copy `.env.example` to `.env` for custom configuration: + +```bash +cp .env.example .env +``` + +Environment variables: +- `PORT` - Server port (default: 8000) +- `LLM_MODEL` - LLM model to use (default: gemini-2.0-flash) +- `LOG_REQUESTS` - Enable request logging (default: false) +- `ENABLE_MCP_SERVERS` - Enable MCP server integrations (default: false in containers) + +## API Endpoints + +- `/` - AG-UI agent endpoint (POST) +- `/health` - Health check (GET) +- `/info` - Agent information (GET) + +## Local Development + +### Setup + +```bash +pip install -r requirements.txt +``` + +### Run + +```bash +# With local MCP servers (original version) +python agent.py + +# Container-friendly version +python agent_container.py +``` + +Server runs on `http://localhost:8000` with AG-UI endpoint at `/`. + +## Frontend Integration + +Connect your AG-UI frontend to `http://localhost:8000/`. + +## Container Details + +The Docker image: +- Uses Python 3.13 slim base +- Runs as non-root user (`obol-agent`) +- Includes health checks +- Exposes port 8000 +- Supports both standalone and compose deployment \ No newline at end of file diff --git a/obol-adk/obol-agent-ag-ui/__init__.py b/obol-adk/obol-agent-ag-ui/__init__.py new file mode 100644 index 00000000..8f48362c --- /dev/null +++ b/obol-adk/obol-agent-ag-ui/__init__.py @@ -0,0 +1,4 @@ +"""Obol Agent AG-UI Integration""" +from .agent import obol_agent, adk_agent, app + +__all__ = ["obol_agent", "adk_agent", "app"] diff --git a/obol-adk/obol-agent-ag-ui/agent.py b/obol-adk/obol-agent-ag-ui/agent.py new file mode 100644 index 00000000..04b7505f --- /dev/null +++ b/obol-adk/obol-agent-ag-ui/agent.py @@ -0,0 +1,195 @@ +from google.adk.agents.llm_agent import LlmAgent +from google.adk.tools.mcp_tool.mcp_toolset import McpToolset, StdioConnectionParams +from mcp.client.stdio import StdioServerParameters +from ag_ui_adk import ADKAgent, add_adk_fastapi_endpoint +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +import uvicorn +import os +import json +from dotenv import load_dotenv + +# Load environment variables - check parent dir first (local dev), then current dir (Docker) +env_path = os.path.join(os.path.dirname(__file__), '..', '.env') +if not os.path.exists(env_path): + env_path = os.path.join(os.path.dirname(__file__), '.env') +load_dotenv(env_path) + +# Create core tools +core_tools = [ + # Obol MCP Server for debugging Obol clusters + McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="uvx", + args=["obol-mcp"] + ) + ) + ) +] + +# Add filesystem MCP servers for each configured path +filesystem_paths = os.getenv('FILESYSTEM_MCP_PATHS', '') +if filesystem_paths: + for path in filesystem_paths.split(','): + path = path.strip() + if path and os.path.exists(path): + core_tools.append( + McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command='npx', + args=["-y", "@modelcontextprotocol/server-filesystem", path] + ) + ) + ) + ) + +# Optional tools +optional_tools = [] + +# Check if running in public mode (e.g., DV Launchpad) +# When PUBLIC_MODE=true, skip Kubernetes tools for security +public_mode = os.getenv('PUBLIC_MODE', 'false').lower() == 'true' + +# Add Kubernetes MCP Server only in private mode (local deployments) +if not public_mode: + try: + optional_tools.append( + McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="uvx", + args=["mcp-server-kubernetes"] + ) + ) + ) + ) + except Exception: + pass + +# Try to add Foundry MCP Server if available +# try: +# if os.path.exists("/Users/bussyjd/Development/foundry-mcp-server/dist/index.js"): +# optional_tools.append( +# McpToolset( +# connection_params=StdioConnectionParams( +# server_params=StdioServerParameters( +# command="node", +# args=["/Users/bussyjd/Development/foundry-mcp-server/dist/index.js"] +# ) +# ) +# ) +# ) +# except Exception: +# pass + +# Create the LLM agent +obol_agent = LlmAgent( + model='gemini-2.0-flash', + name='obol_agent', + instruction=( + 'You are Obol Agent, an assistant that helps users manage their Obol clusters, various L1 and L2 clients ' + 'running in Kubernetes clusters. ' + '\n\n' + 'CRITICAL TOOL USAGE FOR DOCUMENTATION QUESTIONS:\n' + 'When asked about Obol concepts, SDK usage, cluster setup, configuration, or best practices, you MUST:\n' + '1. First call list_allowed_directories to see what documentation is available\n' + '2. Then use search_files with relevant keywords (e.g., "SDK", "quickstart", "DV", "cluster") OR use list_directory to explore\n' + '3. Read the relevant .md files using read_file\n' + '4. Provide the answer based on what you read\n' + '\n' + 'NEVER ask the user for directory paths or say you lack information - you have list_allowed_directories and search_files tools. ' + 'Use them automatically without asking for permission.\n' + '\n' + 'When providing answers from documentation:\n' + '- DO NOT mention that you are searching or reading files\n' + '- Just provide the information naturally as if you know it\n' + '- Be comprehensive and helpful\n' + '\n' + 'For Kubernetes queries, use kubectl tools. For Obol cluster operations, use obol tools. ' + 'Use the appropriate tool based on the user query.' + ), + tools=core_tools + optional_tools +) + +# Wrap agent with AG-UI middleware +adk_agent = ADKAgent( + adk_agent=obol_agent, + app_name="obol_ag_ui_app", + user_id="default_user", + session_timeout_seconds=3600, + use_in_memory_services=True +) + +# Create FastAPI app +app = FastAPI(title="Obol Agent - AG UI Backend") + +# Add CORS middleware for browser-based clients +# This allows dv-launchpad to connect directly from the browser +cors_origins = os.getenv('CORS_ORIGINS', '').split(',') if os.getenv('CORS_ORIGINS') else [ + "http://localhost:3000", # Local dev + "http://localhost:3001", # Alternative dev port + "https://launchpad.obol.org", # Production + "https://dev.launchpad.obol.org", # Dev environment + "https://qa.launchpad.obol.org", # QA environment + "https://mainnet.launchpad.obol.org", # Mainnet + "https://holesky.launchpad.obol.org", # Testnet + "https://sepolia.launchpad.obol.org", # Testnet + "https://secret.launchpad.obol.org", # Secret testnet + "https://gnosis.launchpad.obol.org", # Gnosis chain +] + +# Remove empty strings from origins list +cors_origins = [origin.strip() for origin in cors_origins if origin.strip()] + +app.add_middleware( + CORSMiddleware, + allow_origins=cors_origins, + allow_credentials=True, + allow_methods=["POST", "OPTIONS", "GET"], + allow_headers=["*"], + max_age=3600, + expose_headers=["content-type", "content-length"], +) + +# Add request logging middleware +@app.middleware("http") +async def log_requests(request: Request, call_next): + if request.method == "POST" and request.url.path == "/": + body = await request.body() + print(f"Incoming request to /: {body[:500]}") # Log first 500 chars + response = await call_next(request) + return response + +# Add ADK endpoint +add_adk_fastapi_endpoint( + app=app, + agent=adk_agent, + path="/" +) + +# Startup event to eagerly initialize MCP toolsets +@app.on_event("startup") +async def warmup_mcp_tools(): + """Warm up MCP toolsets during startup to avoid first-request delays/errors""" + print("Warming up MCP toolsets...") + try: + # Trigger tool discovery by getting tools from each MCP toolset + for tool in core_tools + optional_tools: + if isinstance(tool, McpToolset): + # This triggers the async connection and tool discovery + await tool.get_tools(context=None) + print("✓ MCP toolsets warmed up successfully") + except Exception as e: + print(f"⚠ Warning: MCP warmup encountered an error (will retry on first request): {e}") + +# Health check endpoint +@app.get("/health") +async def health_check(): + return {"status": "healthy", "agent": obol_agent.name} + +if __name__ == "__main__": + print("Starting Obol Agent AG-UI backend on http://localhost:8000") + print("AG-UI endpoint available at: http://localhost:8000/") + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/obol-adk/obol-agent-ag-ui/cloudbuild.yaml b/obol-adk/obol-agent-ag-ui/cloudbuild.yaml new file mode 100644 index 00000000..21b556f6 --- /dev/null +++ b/obol-adk/obol-agent-ag-ui/cloudbuild.yaml @@ -0,0 +1,62 @@ +steps: + # Build the container image + - name: 'gcr.io/cloud-builders/docker' + args: + - 'build' + - '-t' + - 'us-east4-docker.pkg.dev/$PROJECT_ID/obol-agent/obol-agent-ag-ui:$SHORT_SHA' + - '-t' + - 'us-east4-docker.pkg.dev/$PROJECT_ID/obol-agent/obol-agent-ag-ui:latest' + - '.' + + # Push the container image to Artifact Registry + - name: 'gcr.io/cloud-builders/docker' + args: + - 'push' + - '--all-tags' + - 'us-east4-docker.pkg.dev/$PROJECT_ID/obol-agent/obol-agent-ag-ui' + + # Deploy to Cloud Run + - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' + entrypoint: gcloud + args: + - 'run' + - 'deploy' + - 'obol-agent-ag-ui' + - '--image' + - 'us-east4-docker.pkg.dev/$PROJECT_ID/obol-agent/obol-agent-ag-ui:$SHORT_SHA' + - '--region' + - 'us-east4' + - '--platform' + - 'managed' + - '--memory' + - '1Gi' + - '--cpu' + - '1' + - '--min-instances' + - '0' + - '--max-instances' + - '10' + - '--port' + - '8000' + - '--timeout' + - '300' + - '--concurrency' + - '80' + - '--service-account' + - 'obol-agent-sa@$PROJECT_ID.iam.gserviceaccount.com' + - '--set-secrets' + - 'GOOGLE_API_KEY=google-api-key:latest' + - '--set-env-vars' + - 'PUBLIC_MODE=true' + - '--allow-unauthenticated' + +images: + - 'us-east4-docker.pkg.dev/$PROJECT_ID/obol-agent/obol-agent-ag-ui:$SHORT_SHA' + - 'us-east4-docker.pkg.dev/$PROJECT_ID/obol-agent/obol-agent-ag-ui:latest' + +options: + machineType: 'E2_HIGHCPU_8' + logging: CLOUD_LOGGING_ONLY + +timeout: '1200s' diff --git a/obol-adk/obol-agent-ag-ui/requirements.txt b/obol-adk/obol-agent-ag-ui/requirements.txt new file mode 100644 index 00000000..5d598739 --- /dev/null +++ b/obol-adk/obol-agent-ag-ui/requirements.txt @@ -0,0 +1,5 @@ +google-adk>=1.16.0 +ag-ui-adk>=0.3.1 +fastapi>=0.118.3 +uvicorn[standard]>=0.37.0 +python-dotenv>=1.1.1 diff --git a/obol-adk/obol-agent-ag-ui/test_agent.py b/obol-adk/obol-agent-ag-ui/test_agent.py new file mode 100755 index 00000000..230cc40d --- /dev/null +++ b/obol-adk/obol-agent-ag-ui/test_agent.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +""" +Test script for Obol Agent AG-UI endpoint +""" +import requests +import json +import sys +from typing import Dict, List, Any + + +class ObolAgentTester: + def __init__(self, base_url: str = "http://localhost:8000"): + self.base_url = base_url + self.session = requests.Session() + + def check_health(self) -> bool: + """Check if the agent is healthy""" + try: + response = self.session.get(f"{self.base_url}/health") + if response.status_code == 200: + data = response.json() + print(f"✓ Health check passed: {data}") + return True + else: + print(f"✗ Health check failed: {response.status_code}") + return False + except Exception as e: + print(f"✗ Health check error: {e}") + return False + + def send_message(self, message: str, thread_id: str = "test-thread") -> Dict[str, Any]: + """Send a message to the agent and collect streaming response""" + payload = { + "threadId": thread_id, + "runId": f"run-{thread_id}", + "tools": [], + "context": [], + "forwardedProps": {"config": {}, "threadMetadata": {}}, + "state": {}, + "messages": [ + { + "id": "msg-1", + "role": "user", + "content": message + } + ] + } + + try: + response = self.session.post( + f"{self.base_url}/", + json=payload, + stream=True, + headers={"Content-Type": "application/json"} + ) + + if response.status_code != 200: + return { + "success": False, + "error": f"HTTP {response.status_code}", + "response": None + } + + # Collect all streaming events + events = [] + full_text = "" + + for line in response.iter_lines(): + if line: + line_str = line.decode('utf-8') + if line_str.startswith('data: '): + event_data = json.loads(line_str[6:]) + events.append(event_data) + + # Collect text content + if event_data.get('type') == 'TEXT_MESSAGE_CONTENT': + full_text += event_data.get('delta', '') + + return { + "success": True, + "events": events, + "full_text": full_text.strip() + } + + except Exception as e: + return { + "success": False, + "error": str(e), + "response": None + } + + def run_tests(self): + """Run a suite of tests""" + print("=" * 60) + print("Obol Agent AG-UI Test Suite") + print("=" * 60) + print() + + # Test 1: Health check + print("Test 1: Health Check") + print("-" * 60) + if not self.check_health(): + print("✗ Cannot proceed with tests - agent is not healthy") + return False + print() + + # Test 2: List available tools + print("Test 2: List Available Tools") + print("-" * 60) + result = self.send_message("What tools do you have access to?") + if result['success']: + print(f"✓ Response received ({len(result['events'])} events)") + print(f"Full text preview: {result['full_text'][:200]}...") + + # Check for expected tools + text = result['full_text'].lower() + expected_tools = ['obol', 'filesystem', 'kubectl'] + found_tools = [tool for tool in expected_tools if tool in text] + print(f"✓ Found tools: {', '.join(found_tools)}") + else: + print(f"✗ Failed: {result.get('error')}") + print() + + # Test 3: Query Obol cluster information + print("Test 3: Query Obol API") + print("-" * 60) + result = self.send_message("Can you list the available Obol API functions?") + if result['success']: + print(f"✓ Response received ({len(result['events'])} events)") + print(f"Preview: {result['full_text'][:300]}...") + else: + print(f"✗ Failed: {result.get('error')}") + print() + + # Test 4: Documentation access (filesystem) + print("Test 4: Documentation Access") + print("-" * 60) + result = self.send_message("Search for documentation about 'quickstart' or 'getting started'") + if result['success']: + print(f"✓ Response received ({len(result['events'])} events)") + print(f"Preview: {result['full_text'][:300]}...") + else: + print(f"✗ Failed: {result.get('error')}") + print() + + print("=" * 60) + print("Test Suite Complete") + print("=" * 60) + return True + + +def main(): + # Check if custom URL provided + base_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8000" + + tester = ObolAgentTester(base_url) + success = tester.run_tests() + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/obol-adk/obol-agent-web/.env.example b/obol-adk/obol-agent-web/.env.example new file mode 100644 index 00000000..df599322 --- /dev/null +++ b/obol-adk/obol-agent-web/.env.example @@ -0,0 +1,23 @@ +# Obol Agent Web Configuration +# Copy this file to .env and adjust values as needed + +# Agent Configuration +OBOL_AGENT_MODEL=gemini-2.0-flash + +# Workspace Paths +OBOL_WORKSPACE_PATH=/Users/bussyjd/Development/Obol_Workbench/obol-stack +OBOL_DOCS_PATH=/Users/bussyjd/Development/Obol_Workbench/obol-stack/obol-adk/docs + +# Kubernetes Configuration +KUBECONFIG=/Users/bussyjd/.kube/config + +# Obol MCP Server Configuration +OBOL_MCP_LOG_LEVEL=INFO +OBOL_CACHE_TTL=300 +OBOL_API_BASE_URL=https://api.obol.tech +OBOL_REQUEST_TIMEOUT=15.0 +OBOL_RATE_LIMIT_DELAY=0.1 + +# ADK Server Configuration +PORT=8080 +LOG_LEVEL=INFO \ No newline at end of file diff --git a/obol-adk/obol-agent-web/README.md b/obol-adk/obol-agent-web/README.md new file mode 100644 index 00000000..8c971116 --- /dev/null +++ b/obol-adk/obol-agent-web/README.md @@ -0,0 +1,202 @@ +# Obol Agent Web Interface + +A comprehensive AI agent for Obol Distributed Validator management accessible via Google ADK's web interface. + +## 🚀 Features + +The Obol Agent provides access to four powerful MCP (Model Context Protocol) servers: + +### 🔗 Available Tools + +1. **📁 Filesystem MCP** (Official MCP Community) + - File operations and project management + - Read/write files across the entire workspace + - Directory navigation and file manipulation + - Maintained by the [MCP community](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) for reliability + +2. **🌐 Enhanced Obol MCP** + - Obol API integration with caching and rate limiting + - Cluster effectiveness monitoring + - Network status and lock management + - Terms and conditions checking + +3. **☸️ Kubernetes MCP** (Official MCP Community) + - Container orchestration and cluster management + - Pod, deployment, and service operations + - Kubernetes resource monitoring + - Automatic KUBECONFIG mounting for cluster access + +4. **⚒️ Foundry MCP** + - Smart contract development and testing + - Forge build, test, and deployment tools + - Solidity development support + +## ⚙️ Configuration + +### Environment Variables + +Copy `.env.example` to `.env` and customize: + +```bash +cp .env.example .env +``` + +Key configuration options: + +| Variable | Description | Default | +|----------|-------------|---------| +| `OBOL_AGENT_MODEL` | LLM model to use | `gemini-2.0-flash` | +| `OBOL_WORKSPACE_PATH` | Project workspace directory | Current project path | +| `OBOL_DOCS_PATH` | Documentation directory | `workspace/obol-adk/docs` | +| `KUBECONFIG` | Kubernetes config file path | `~/.kube/config` | +| `OBOL_MCP_LOG_LEVEL` | Obol MCP logging level | `INFO` | +| `OBOL_CACHE_TTL` | Obol API cache TTL (seconds) | `300` | + +## 🚀 Quick Start + +### Prerequisites + +1. **Docker** - All MCP servers run in containers +2. **Google ADK** - Installed via `pip install google-adk` +3. **Built MCP Images** - Run the build script or use our workflow + +### Pull and Build Required Images + +```bash +# Pull official MCP filesystem server (maintained by MCP community) +docker pull mcp/filesystem + +# Build custom MCP servers +cd /path/to/obol-stack + +# Kubernetes MCP +docker build -f obol-adk/dockerfiles/Dockerfile.kubernetes -t kubernetes-mcp:latest obol-adk/dockerfiles + +# Foundry MCP +docker build -f obol-adk/dockerfiles/Dockerfile.foundry -t foundry-mcp:latest obol-adk/dockerfiles + +# Enhanced Obol MCP +docker build -f obol-adk/obol-agent-docker/Dockerfile -t obol-mcp:enhanced obol-adk/obol-agent-docker +``` + +### Launch Web Interface + +```bash +# Navigate to project root +cd /path/to/obol-stack/obol-adk + +# Start the web interface +adk web + +# Or with custom configuration +OBOL_AGENT_MODEL=gemini-1.5-pro adk web +``` + +The web interface will be available at `http://localhost:8080` + +## 🛠️ Usage Examples + +### Obol Cluster Management +``` +"Check the health of the Obol API and show me effectiveness metrics for mainnet clusters" +``` + +### Kubernetes Operations +``` +"List all pods in the default namespace and show me the status of my deployments" +``` + +### Smart Contract Development +``` +"Create a new Foundry project and help me write a simple ERC20 token contract" +``` + +### File Management +``` +"Show me the structure of the docs directory and help me create a new documentation file" +``` + +## 🔧 Advanced Configuration + +### Custom Docker Mounts + +Modify `agent.py` to add custom volume mounts: + +```python +# Add custom mounts for specific use cases +"-v", "/custom/path:/mount/point", +``` + +### MCP Server Configuration + +Configure individual MCP servers via environment variables: + +```bash +# Obol MCP with debug logging and extended cache +OBOL_MCP_LOG_LEVEL=DEBUG OBOL_CACHE_TTL=600 adk web + +# Custom Kubernetes config +KUBECONFIG=/path/to/custom/kubeconfig adk web +``` + +## 🏗️ Architecture + +``` +Web Browser + ↓ +Google ADK Web Interface (FastAPI) + ↓ +Obol Agent (LlmAgent) + ↓ +MCPToolset Connections + ↓ +Docker MCP Servers (stdio) + ↓ +External APIs/Tools +``` + +## 🐛 Troubleshooting + +### Common Issues + +1. **Docker Images Not Found** + ```bash + # Rebuild images + docker build -f obol-adk/dockerfiles/Dockerfile.filesystem -t filesystem-mcp:latest obol-adk/dockerfiles + ``` + +2. **Kubernetes Access Issues** + ```bash + # Check kubeconfig + kubectl cluster-info + export KUBECONFIG=/path/to/working/kubeconfig + ``` + +3. **Permission Issues** + ```bash + # Fix workspace permissions + chmod -R 755 /path/to/workspace + ``` + +4. **Agent Not Responding** + ```bash + # Check logs + LOG_LEVEL=DEBUG adk web + ``` + +### Logs and Debugging + +- Set `LOG_LEVEL=DEBUG` for verbose logging +- Check Docker container logs: `docker logs ` +- Monitor MCP connections in the ADK web interface + +## 🤝 Contributing + +1. Fork the repository +2. Create your feature branch +3. Test with `adk web` +4. Submit a pull request + +## 📝 License + +Licensed under the same terms as the main project. \ No newline at end of file diff --git a/obol-adk/obol-agent-web/__init__.py b/obol-adk/obol-agent-web/__init__.py new file mode 100644 index 00000000..63bd45e6 --- /dev/null +++ b/obol-adk/obol-agent-web/__init__.py @@ -0,0 +1 @@ +from . import agent \ No newline at end of file diff --git a/obol-adk/obol-agent-web/agent.py b/obol-adk/obol-agent-web/agent.py new file mode 100644 index 00000000..047d9274 --- /dev/null +++ b/obol-adk/obol-agent-web/agent.py @@ -0,0 +1,86 @@ +from google.adk.agents.llm_agent import LlmAgent +from google.adk.tools.mcp_tool.mcp_toolset import McpToolset, StdioConnectionParams +from mcp.client.stdio import StdioServerParameters +import os +from dotenv import load_dotenv + +# Load environment variables from parent directory .env file +env_path = os.path.join(os.path.dirname(__file__), '..', '.env') +load_dotenv(env_path) + +# Create core tools +core_tools = [ + # Obol MCP Server for debugging Obol clusters + McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="uvx", + args=["obol-mcp"] + ) + ) + ) +] + +# Add filesystem MCP servers for each configured path +filesystem_paths = os.getenv('FILESYSTEM_MCP_PATHS', '') +if filesystem_paths: + for path in filesystem_paths.split(','): + path = path.strip() + if path and os.path.exists(path): + core_tools.append( + McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command='npx', + args=["-y", "@modelcontextprotocol/server-filesystem", path] + ) + ) + ) + ) + +# Optional tools +optional_tools = [] + +# Try to add Kubernetes MCP Server if available +try: + optional_tools.append( + McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="uvx", + args=["mcp-server-kubernetes"] + ) + ) + ) + ) +except Exception: + pass + +# Create the LLM agent +root_agent = LlmAgent( + model='gemini-2.0-flash', + name='obol_agent', + instruction=( + 'You are Obol Agent, an assistant that helps users manage their Obol clusters, various L1 and L2 clients ' + 'running in Kubernetes clusters. ' + '\n\n' + 'CRITICAL TOOL USAGE FOR DOCUMENTATION QUESTIONS:\n' + 'When asked about Obol concepts, SDK usage, cluster setup, configuration, or best practices, you MUST:\n' + '1. First call list_allowed_directories to see what documentation is available\n' + '2. Then use search_files with relevant keywords (e.g., "SDK", "quickstart", "DV", "cluster") OR use list_directory to explore\n' + '3. Read the relevant .md files using read_file\n' + '4. Provide the answer based on what you read\n' + '\n' + 'NEVER ask the user for directory paths or say you lack information - you have list_allowed_directories and search_files tools. ' + 'Use them automatically without asking for permission.\n' + '\n' + 'When providing answers from documentation:\n' + '- DO NOT mention that you are searching or reading files\n' + '- Just provide the information naturally as if you know it\n' + '- Be comprehensive and helpful\n' + '\n' + 'For Kubernetes queries, use kubectl tools. For Obol cluster operations, use obol tools. ' + 'Use the appropriate tool based on the user query.' + ), + tools=core_tools + optional_tools +) diff --git a/obol-adk/obol-agent-web/agent_k8s.py b/obol-adk/obol-agent-web/agent_k8s.py new file mode 100644 index 00000000..39a4deaf --- /dev/null +++ b/obol-adk/obol-agent-web/agent_k8s.py @@ -0,0 +1,79 @@ +# Enhanced Obol Agent for Kubernetes deployment +import os +from google.adk.agents.llm_agent import LlmAgent +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters + +# Configuration - Environment variables for flexibility +WORKSPACE_PATH = os.getenv("OBOL_WORKSPACE_PATH", "/workspace") +DOCS_PATH = os.getenv("OBOL_DOCS_PATH", f"{WORKSPACE_PATH}/obol-adk/docs") +USE_IN_CLUSTER_CONFIG = os.getenv("USE_IN_CLUSTER_CONFIG", "false").lower() == "true" + +# Agent definition for Kubernetes deployment +root_agent = LlmAgent( + model=os.getenv("OBOL_AGENT_MODEL", "gemini-2.5-flash-preview-05-20"), + name='obol_agent', + instruction=( + 'You are Obol Agent running in Kubernetes, a comprehensive assistant specialized in distributed validator technology. ' + 'You help users manage:\n' + '• Obol Distributed Validator clusters and networks\n' + '• Kubernetes deployments and container orchestration\n' + '• Foundry smart contract development and testing\n' + '• File system operations and project management\n\n' + 'Use the most appropriate tool(s) for each user query. You have access to:\n' + '- Obol API for cluster management and network status\n' + '- Kubernetes tools for container orchestration\n' + '- Foundry tools for smart contract development\n' + '- File system tools for project management\n\n' + 'Always provide clear, actionable responses and suggest relevant follow-up actions.' + ), + tools=[ + # Filesystem MCP - File operations and project management + MCPToolset( + connection_params=StdioServerParameters( + command='docker', + args=[ + "run", "-i", "--rm", + "--mount", f"type=bind,src={WORKSPACE_PATH},dst=/projects/workspace", + "mcp/filesystem", + "/projects" + ], + ), + ), + + # Enhanced Obol MCP - Obol API and cluster management + MCPToolset( + connection_params=StdioServerParameters( + command="docker", + args=[ + "run", "--rm", "-i", + "obol-mcp:enhanced" + ], + ), + ), + + # Kubernetes MCP - Using kubectl directly instead of docker for in-cluster access + MCPToolset( + connection_params=StdioServerParameters( + command="kubectl", + args=["exec", "-i", "kubernetes-mcp-0", "--", "kubernetes-mcp-server"] + if USE_IN_CLUSTER_CONFIG else + [ + "run", "--rm", "-i", + "-v", f"{os.path.expanduser('~/.kube/config')}:/home/appuser/.kube/config:ro", + "-e", "KUBECONFIG=/home/appuser/.kube/config", + "mcp/kubernetes:latest" + ], + ), + ) if not USE_IN_CLUSTER_CONFIG else + # For in-cluster, use the Node.js MCP server directly + MCPToolset( + connection_params=StdioServerParameters( + command="kubernetes-mcp-server", + args=[] + ), + ), + ], +) + +# Export the agent for ADK to discover +__all__ = ['root_agent'] \ No newline at end of file diff --git a/obol-adk/requirements.txt b/obol-adk/requirements.txt new file mode 100644 index 00000000..a793c087 --- /dev/null +++ b/obol-adk/requirements.txt @@ -0,0 +1,2 @@ +google-adk>=1.16.0 + diff --git a/obol-adk/scripts/setup_dev.py b/obol-adk/scripts/setup_dev.py new file mode 100755 index 00000000..5f4142fc --- /dev/null +++ b/obol-adk/scripts/setup_dev.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +""" +Development setup script for obol-adk +Clones/updates obol-gitbook and configures .env for local development +""" +import os +import subprocess +import sys +from pathlib import Path + + +def run_command(cmd, cwd=None, check=True): + """Run a shell command and return result""" + try: + result = subprocess.run( + cmd, + shell=True, + cwd=cwd, + check=check, + capture_output=True, + text=True + ) + return result.returncode == 0, result.stdout, result.stderr + except subprocess.CalledProcessError as e: + return False, e.stdout, e.stderr + + +def setup_gitbook(base_dir): + """Clone or update obol-gitbook repository""" + gitbook_dir = base_dir / "obol-gitbook" + + if gitbook_dir.exists(): + print(f"📚 Updating obol-gitbook...") + success, stdout, stderr = run_command("git pull", cwd=gitbook_dir) + if success: + print(f"✓ obol-gitbook updated") + else: + print(f"⚠ Failed to update obol-gitbook: {stderr}") + return False + else: + print(f"📚 Cloning obol-gitbook...") + success, stdout, stderr = run_command( + "git clone https://github.com/ObolNetwork/obol-gitbook.git obol-gitbook", + cwd=base_dir + ) + if success: + print(f"✓ obol-gitbook cloned to {gitbook_dir}") + else: + print(f"✗ Failed to clone obol-gitbook: {stderr}") + return False + + return True + + +def update_env_file(base_dir, gitbook_path): + """Update .env file with FILESYSTEM_MCP_PATHS""" + env_file = base_dir / ".env" + env_example = base_dir / ".env.example" + + # Read existing .env or create from example + env_lines = [] + if env_file.exists(): + with open(env_file, 'r') as f: + env_lines = f.readlines() + elif env_example.exists(): + with open(env_example, 'r') as f: + env_lines = f.readlines() + + # Check if FILESYSTEM_MCP_PATHS already exists + has_filesystem_paths = any('FILESYSTEM_MCP_PATHS' in line for line in env_lines) + + if not has_filesystem_paths: + # Add FILESYSTEM_MCP_PATHS + if env_lines and not env_lines[-1].endswith('\n'): + env_lines.append('\n') + env_lines.append(f'\n# Filesystem MCP Server Paths\n') + env_lines.append(f'FILESYSTEM_MCP_PATHS={gitbook_path}\n') + + with open(env_file, 'w') as f: + f.writelines(env_lines) + print(f"✓ Added FILESYSTEM_MCP_PATHS to {env_file}") + else: + print(f"✓ FILESYSTEM_MCP_PATHS already configured in {env_file}") + + # Ensure GOOGLE_API_KEY is present + has_api_key = any('GOOGLE_API_KEY' in line and not line.strip().startswith('#') for line in env_lines) + if not has_api_key: + print(f"⚠ Warning: GOOGLE_API_KEY not found in .env") + print(f" Add your API key to {env_file}:") + print(f" GOOGLE_API_KEY=your-api-key-here") + + return True + + +def main(): + """Main setup function""" + # Get the obol-adk directory (parent of scripts/) + script_dir = Path(__file__).parent + base_dir = script_dir.parent + + print("=" * 60) + print("Obol ADK Development Setup") + print("=" * 60) + print() + + # Setup gitbook + if not setup_gitbook(base_dir): + sys.exit(1) + + # Update .env + gitbook_path = str(base_dir / "obol-gitbook") + if not update_env_file(base_dir, gitbook_path): + sys.exit(1) + + print() + print("=" * 60) + print("✓ Development setup complete!") + print("=" * 60) + print() + print("Next steps:") + print(f" 1. Ensure GOOGLE_API_KEY is set in {base_dir}/.env") + print(f" 2. Run: make start") + print() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/obolup/manifests/expose-agent.yaml b/obolup/manifests/expose-agent.yaml new file mode 100644 index 00000000..f324c4d3 --- /dev/null +++ b/obolup/manifests/expose-agent.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: expose-agent + namespace: default + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + ingressClassName: nginx + rules: + - host: obol.stack + http: + paths: + - path: /agent(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: obol-agent + port: + number: 8000 \ No newline at end of file diff --git a/values/hoodi/erigon.yaml b/values/hoodi/erigon.yaml new file mode 100644 index 00000000..2326f172 --- /dev/null +++ b/values/hoodi/erigon.yaml @@ -0,0 +1,29 @@ +# Hoodi network override values for Erigon +# This file should be used with -f mainnet/erigon.yaml -f hoodi/erigon.yaml + +# -- Extra args for the erigon container +extraArgs: + - --chain=holesky +# -- Extra args for the rpcdaemon container +extraArgsRPCDaemon: + - --http.api=eth,erigon,web3,net,txpool,engine,trace + +# Increase probe delays for initial sync +livenessProbe: + tcpSocket: + port: metrics + initialDelaySeconds: 900 + periodSeconds: 30 + failureThreshold: 3 + +readinessProbe: + tcpSocket: + port: metrics + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 30 + +# Enable persistence with appropriate size for testnet +persistence: + enabled: true + size: 350Gi \ No newline at end of file diff --git a/values/mainnet/erigon.yaml b/values/mainnet/erigon.yaml new file mode 100644 index 00000000..445c70e4 --- /dev/null +++ b/values/mainnet/erigon.yaml @@ -0,0 +1,394 @@ +# -- Overrides the chart's name +nameOverride: "" + +# -- Overrides the chart's computed fullname +fullnameOverride: "" + +# -- Number of replicas +replicas: 1 + +image: + # -- erigon container image repository + repository: erigontech/erigon + # -- erigon container image tag + tag: v3.0.5 + # -- erigon container pull policy + pullPolicy: IfNotPresent + +# -- Extra args for the erigon container +extraArgs: [] + #- --holesky + +# -- JWT secret used by client as a secret. Change this value. +jwt: ecb22bc24e7d4061f7ed690ccd5846d7d73f5d2b9733267e12f56790398d908a + +# -- Extra args for the rpcdaemon container +extraArgsRPCDaemon: [] + #- --http.corsdomain=yourdomain.tld + #- --http.api=eth,erigon,web3,net,debug,trace,txpool,db + +# -- Template used for the default command +# @default -- See `values.yaml` +defaultCommandTemplate: | + - sh + - -ac + - > + {{- if .Values.p2pNodePort.enabled }} + . /env/init-nodeport; + {{- end }} + exec erigon + --datadir=/data + {{- if .Values.p2pNodePort.enabled }} + {{- if not (contains "--nat=" (.Values.extraArgs | join ",")) }} + --nat=extip:$EXTERNAL_IP + {{- end }} + {{- if not (contains "--port=" (.Values.extraArgs | join ",")) }} + --port=$EXTERNAL_PORT + {{- end }} + {{- else }} + {{- if not (contains "--nat=" (.Values.extraArgs | join ",")) }} + --nat=extip:$(POD_IP) + {{- end }} + {{- if not (contains "--port=" (.Values.extraArgs | join ",")) }} + --port={{ include "erigon.p2pPort" . }} + {{- end }} + {{- end }} + --http=false + --private.api.addr=127.0.0.1:9090 + --authrpc.jwtsecret=/data/jwt.hex + --authrpc.addr=0.0.0.0 + --authrpc.port={{ .Values.authPort }} + --authrpc.vhosts=* + --ws + --metrics + --metrics.addr=0.0.0.0 + --metrics.port={{ .Values.metricsPort }} + {{- range .Values.extraArgs }} + {{ tpl . $ }} + {{- end }} + +# -- Template used for the default command +# @default -- See `values.yaml` +defaultCommandRPCDaemonTemplate: | + - sh + - -ac + - > + until timeout 1 bash -c ">/dev/tcp/127.0.0.1/9090" 2>/dev/null; do echo "Waiting for erigon..."; sleep 1; done; + exec rpcdaemon + --datadir=/data + --private.api.addr=127.0.0.1:9090 + --txpool.api.addr=127.0.0.1:9090 + --http.addr=0.0.0.0 + --http.port={{ .Values.httpPort }} + --http.vhosts=* + --ws + --metrics + --metrics.addr=0.0.0.0 + --metrics.port={{ .Values.metricsPortRPCDaemon }} + {{- range .Values.extraArgsRPCDaemon }} + {{ tpl . $ }} + {{- end }} + +# -- Legacy way of overwriting the default command. You may prefer to change defaultCommandTemplate instead. +customCommand: [] + +# -- Legacy way of overwriting the default command. You may prefer to change defaultCommandRPCDaemonTemplate instead. +customCommandRPCDaemon: [] # Only change this if you need to change the default command + +# When p2pNodePort is enabled, your P2P port will be exposed via service type NodePort. +# This is useful if you want to expose and announce your node to the Internet. +# Limitation: You can only one have one replica when exposing via NodePort. +# Check the chart README.md for more details +p2pNodePort: + # -- Expose P2P port via NodePort + enabled: false + # -- NodePort to be used + port: 31000 + initContainer: + image: + # -- Container image to fetch nodeport information + repository: lachlanevenson/k8s-kubectl + # -- Container tag + tag: v1.25.4 + # -- Container pull policy + pullPolicy: IfNotPresent + portForwardContainer: + image: + # -- Container image for the port forwarder + repository: alpine/socat + # -- Container tag + tag: latest + # -- Container pull policy + pullPolicy: IfNotPresent + +ingress: + # -- Ingress resource for the HTTP API + enabled: false + # -- Annotations for Ingress + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # -- Ingress host + hosts: + - host: chart-example.local + paths: [] + # -- Ingress TLS + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# -- Affinity configuration for pods +affinity: {} + +# -- Image pull secrets for Docker images +imagePullSecrets: [] + +# -- Annotations for the StatefulSet +annotations: {} + +# -- Liveness probe +# @default -- See `values.yaml` +livenessProbe: + tcpSocket: + port: metrics + initialDelaySeconds: 60 + periodSeconds: 120 + +# -- Readiness probe +# @default -- See `values.yaml` +readinessProbe: + tcpSocket: + port: metrics + initialDelaySeconds: 10 + periodSeconds: 10 + +# -- Liveness probe +# @default -- See `values.yaml` +livenessProbeRPCDaemon: + tcpSocket: + port: http-rpc + initialDelaySeconds: 60 + periodSeconds: 120 + +# -- Readiness probe +# @default -- See `values.yaml` +readinessProbeRPCDaemon: + tcpSocket: + port: http-rpc + initialDelaySeconds: 10 + periodSeconds: 10 + +# -- P2P Port +p2pPort: 30303 + +# -- HTTP Port +httpPort: 8545 + +# -- Engine Port (Auth Port) +authPort: 8551 + +# -- Metrics Port +metricsPort: 6060 + +# -- RPC Daemon Port +metricsPortRPCDaemon: 6061 + +# -- Node selector for pods +nodeSelector: {} + +persistence: + # -- Uses an EmptyDir when not enabled + enabled: false + # -- Use an existing PVC when persistence.enabled + existingClaim: null + # -- Access mode for the volume claim template + accessModes: + - ReadWriteOnce + # -- Requested size for volume claim template + size: 20Gi + # -- Use a specific storage class + # E.g 'local-path' for local storage to achieve best performance + # Read more (https://github.com/rancher/local-path-provisioner) + storageClassName: null + # -- Annotations for volume claim template + annotations: {} + # -- Selector for volume claim template + selector: {} + # matchLabels: + # app.kubernetes.io/name: something + +# -- Pod labels +podLabels: {} + +# -- Pod annotations +podAnnotations: {} + +# -- Pod management policy +podManagementPolicy: OrderedReady + +# -- Pod priority class +priorityClassName: null + +rbac: + # -- Specifies whether RBAC resources are to be created + create: true + # -- Required ClusterRole rules + # @default -- See `values.yaml` + clusterRules: + # Required to obtain the nodes external IP + - apiGroups: [""] + resources: + - "nodes" + verbs: + - "get" + - "list" + - "watch" + # -- Required ClusterRole rules + # @default -- See `values.yaml` + rules: + # Required to get information about the services nodePort. + - apiGroups: [""] + resources: + - "services" + verbs: + - "get" + - "list" + - "watch" + +# -- Resource requests and limits for the erigon container +resources: {} +# limits: +# cpu: 500m +# memory: 2Gi +# requests: +# cpu: 300m +# memory: 1Gi + +# -- Resource requests and limits for the RPC daemon container +resourcesRPCDaemon: {} +# limits: +# cpu: 500m +# memory: 2Gi +# requests: +# cpu: 300m +# memory: 1Gi + +# -- The security context for pods +# @default -- See `values.yaml` +securityContext: + fsGroup: 10001 + runAsGroup: 10001 + runAsNonRoot: true + runAsUser: 10001 + +# -- The security context for containers +# @default -- See `values.yaml` +containerSecurityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +serviceAccount: + # -- Specifies whether a service account should be created + create: true + # -- Annotations to add to the service account + annotations: {} + # -- The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# -- How long to wait until the pod is forcefully terminated +terminationGracePeriodSeconds: 300 + +# -- Tolerations for pods +## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +tolerations: [] + +# -- Topology Spread Constraints for pods +## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/ +topologySpreadConstraints: [] + +# -- Define the PodDisruptionBudget spec +# If not set then a PodDisruptionBudget will not be created +podDisruptionBudget: {} +# minAvailable: 1 +# maxUnavailable: 1 + +# -- Update strategy for the Statefulset +updateStrategy: + # -- Update strategy type + type: RollingUpdate + +# -- Additional init containers +initContainers: [] +# - name: my-init-container +# image: busybox:latest +# command: ['sh', '-c', 'echo hello'] + +# -- Additional containers +extraContainers: [] + +# -- Additional volumes +extraVolumes: [] + +# -- Additional volume mounts +extraVolumeMounts: [] + +# -- Additional ports. Useful when using extraContainers +extraPorts: [] + +# -- Additional env variables for erigon container +extraEnv: [] + +# -- Additional env variables for RPCDaemon container +extraEnvRPCDaemon: [] + +# -- Additional env variables injected via a created secret +secretEnv: {} +# MY_PASSWORD: supersecret + +initChownData: + # -- Init container to set the correct permissions to access data directories + enabled: true + image: + # -- Container repository + repository: busybox + # -- Container tag + tag: "1.34.0" + # -- Container pull policy + pullPolicy: IfNotPresent + # -- Resource requests and limits + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +serviceMonitor: + # -- If true, a ServiceMonitor CRD is created for a prometheus operator + # https://github.com/coreos/prometheus-operator + enabled: false + # -- Path to scrape + path: /debug/metrics/prometheus + # -- Alternative namespace for ServiceMonitor + namespace: null + # -- Additional ServiceMonitor labels + labels: {} + # -- Additional ServiceMonitor annotations + annotations: {} + # -- ServiceMonitor scrape interval + interval: 1m + # -- ServiceMonitor scheme + scheme: http + # -- ServiceMonitor TLS configuration + tlsConfig: {} + # -- ServiceMonitor scrape timeout + scrapeTimeout: 30s + # -- ServiceMonitor relabelings + relabelings: [] \ No newline at end of file