Spaces:
Running
Running
jedick
commited on
Commit
·
8f7fb71
1
Parent(s):
ed0d360
Add app files
Browse files- .gitignore +8 -0
- Dockerfile +44 -0
- PlotMyData/__init__.py +13 -0
- PlotMyData/agent.py +352 -0
- README.md +4 -3
- entrypoint.sh +31 -0
- functions.R +101 -0
- profile.R +12 -0
- prompts.R +133 -0
- prompts.py +147 -0
- requirements.txt +3 -0
- server.R +134 -0
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Any secret files (including secret.openai-api-key)
|
| 2 |
+
secret.*
|
| 3 |
+
# Copied by Dockerfile from entrypoint.sh
|
| 4 |
+
startup.sh
|
| 5 |
+
# Created by entrypoint.sh
|
| 6 |
+
.Rprofile
|
| 7 |
+
# We can ignore __pycache__
|
| 8 |
+
__pycache__
|
Dockerfile
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Declare the base image
|
| 2 |
+
FROM rocker/r-ver:latest
|
| 3 |
+
|
| 4 |
+
# Considerations for local development: reduce Docker cache size and rebuild time
|
| 5 |
+
# Single RUN directive and two COPY directives
|
| 6 |
+
# Pre-RUN COPY for relatively stable files, post-RUN COPY for app files
|
| 7 |
+
# Avoid other directives like USER and ENV
|
| 8 |
+
# startup.sh activates the virtual environment for running the app
|
| 9 |
+
# Considerations for remote development (HF Spaces Dev Mode)
|
| 10 |
+
# Dev Mode requires useradd, chown and USER
|
| 11 |
+
# Use CMD instead of ENTRYPOINT
|
| 12 |
+
|
| 13 |
+
# Set working directory and copy non-app files
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
COPY requirements.txt entrypoint.sh .
|
| 16 |
+
|
| 17 |
+
# Install Python and system tools
|
| 18 |
+
# Create and activate virtual environment for installing packages
|
| 19 |
+
# Install required Python and R packages
|
| 20 |
+
# Rename startup script and make it executable
|
| 21 |
+
# Add user with uid=1000 and chown /app directory for HF Spaces Dev Mode
|
| 22 |
+
RUN apt-get update && \
|
| 23 |
+
apt-get install -y python3 python3-pip python3-venv screen vim git && \
|
| 24 |
+
apt-get clean && \
|
| 25 |
+
rm -rf /var/lib/apt/lists/* && \
|
| 26 |
+
python3 -m venv /opt/venv && \
|
| 27 |
+
export PATH="/opt/venv/bin:$PATH" && \
|
| 28 |
+
pip --no-cache-dir install -r requirements.txt && \
|
| 29 |
+
R -q -e 'install.packages(c("ellmer", "mcptools", "readr", "ggplot2", "tidyverse"))' && \
|
| 30 |
+
cp entrypoint.sh startup.sh && \
|
| 31 |
+
chmod +x startup.sh && \
|
| 32 |
+
useradd -m -u 1000 user && \
|
| 33 |
+
chown -R user /app
|
| 34 |
+
|
| 35 |
+
# Copy app files with user permissions
|
| 36 |
+
# NOTE: This overwrites all copied files, rendering them non-executable. That is why we
|
| 37 |
+
# created an executable file with a different name (startup.sh) that is not overwritten here.
|
| 38 |
+
COPY --chown=user . /app
|
| 39 |
+
|
| 40 |
+
# Set the user for Dev Mode
|
| 41 |
+
USER user
|
| 42 |
+
|
| 43 |
+
# Set default command (executable file in WORKDIR)
|
| 44 |
+
CMD [ "/app/startup.sh" ]
|
PlotMyData/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import warnings
|
| 3 |
+
import os
|
| 4 |
+
from . import agent
|
| 5 |
+
|
| 6 |
+
# Ensure upload directory exists
|
| 7 |
+
upload_dir = "/tmp/uploads"
|
| 8 |
+
Path(upload_dir).mkdir(parents=True, exist_ok=True)
|
| 9 |
+
# Read, write, execute for owner; read and execute for others
|
| 10 |
+
os.chmod(upload_dir, 0o755)
|
| 11 |
+
|
| 12 |
+
# Suppress Pydantic serialization warnings
|
| 13 |
+
warnings.filterwarnings("ignore", message="Pydantic serializer warnings")
|
PlotMyData/agent.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from google.adk.plugins.save_files_as_artifacts_plugin import SaveFilesAsArtifactsPlugin
|
| 2 |
+
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
|
| 3 |
+
from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams
|
| 4 |
+
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
| 5 |
+
from google.adk.tools.tool_context import ToolContext
|
| 6 |
+
from google.adk.tools.base_tool import BaseTool
|
| 7 |
+
from google.adk.agents.callback_context import CallbackContext
|
| 8 |
+
from google.adk.agents import LlmAgent
|
| 9 |
+
from google.adk.models import LlmResponse, LlmRequest
|
| 10 |
+
from google.adk.models.lite_llm import LiteLlm
|
| 11 |
+
from google.adk.apps import App
|
| 12 |
+
from google.genai import types
|
| 13 |
+
from mcp import ClientSession, StdioServerParameters
|
| 14 |
+
from mcp.types import CallToolResult, TextContent
|
| 15 |
+
from mcp.client.stdio import stdio_client
|
| 16 |
+
from typing import Dict, Any, Optional, Tuple
|
| 17 |
+
from prompts import Root, Run, Data, Plot, Install
|
| 18 |
+
import base64
|
| 19 |
+
import os
|
| 20 |
+
|
| 21 |
+
# Define MCP server parameters
|
| 22 |
+
server_params = StdioServerParameters(
|
| 23 |
+
command="Rscript",
|
| 24 |
+
args=[
|
| 25 |
+
# Use --vanilla to ignore .Rprofile, which is meant for the R instance running mcp_session()
|
| 26 |
+
"--vanilla",
|
| 27 |
+
"server.R",
|
| 28 |
+
],
|
| 29 |
+
)
|
| 30 |
+
# STDIO transport to local R MCP server
|
| 31 |
+
connection_params = StdioConnectionParams(server_params=server_params, timeout=60)
|
| 32 |
+
|
| 33 |
+
# Define model
|
| 34 |
+
# If we're using the OpenAI API, get the value of OPENAI_MODEL_NAME set by entrypoint.sh
|
| 35 |
+
# If we're using an OpenAI-compatible endpoint (Docker Model Runner), use a fake API key
|
| 36 |
+
model = LiteLlm(
|
| 37 |
+
model=os.environ.get("OPENAI_MODEL_NAME", ""),
|
| 38 |
+
api_key=os.environ.get("OPENAI_API_KEY", "fake-API-key"),
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
async def select_r_session(
|
| 43 |
+
callback_context: CallbackContext,
|
| 44 |
+
) -> Optional[types.Content]:
|
| 45 |
+
"""
|
| 46 |
+
Callback function to select the first R session.
|
| 47 |
+
"""
|
| 48 |
+
async with stdio_client(server_params) as (read, write):
|
| 49 |
+
async with ClientSession(read, write) as session:
|
| 50 |
+
await session.initialize()
|
| 51 |
+
await session.call_tool("select_r_session", {"session": 1})
|
| 52 |
+
print("[select_r_session] R session selected!")
|
| 53 |
+
# Return None to allow the LlmAgent's normal execution
|
| 54 |
+
return None
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
async def catch_tool_errors(tool: BaseTool, args: dict, tool_context: ToolContext):
|
| 58 |
+
"""
|
| 59 |
+
Callback function to catch errors from tool calls and turn them into a message.
|
| 60 |
+
Modified from https://github.com/google/adk-python/discussions/795#discussioncomment-13460659
|
| 61 |
+
"""
|
| 62 |
+
try:
|
| 63 |
+
return await tool.run_async(args=args, tool_context=tool_context)
|
| 64 |
+
except Exception as e:
|
| 65 |
+
# Format the error as a tool response
|
| 66 |
+
# https://github.com/google/adk-python/commit/4df926388b6e9ebcf517fbacf2f5532fd73b0f71
|
| 67 |
+
response = CallToolResult(
|
| 68 |
+
# The error has class McpError; use e.error.message to get the text
|
| 69 |
+
content=[TextContent(type="text", text=e.error.message)],
|
| 70 |
+
isError=True,
|
| 71 |
+
)
|
| 72 |
+
return response.model_dump(exclude_none=True, mode="json")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
async def preprocess_artifact(
|
| 76 |
+
callback_context: CallbackContext, llm_request: LlmRequest
|
| 77 |
+
) -> Optional[LlmResponse]:
|
| 78 |
+
"""
|
| 79 |
+
Callback function to copy the latest artifact to a temporary file.
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
# Callback and artifact handling code modified from:
|
| 83 |
+
# https://google.github.io/adk-docs/callbacks/types-of-callbacks/#before-model-callback
|
| 84 |
+
# https://github.com/google/adk-python/issues/2176#issuecomment-3395469070
|
| 85 |
+
|
| 86 |
+
# Get the last user message in the request contents
|
| 87 |
+
last_user_message = llm_request.contents[-1].parts[-1].text
|
| 88 |
+
|
| 89 |
+
# Function call events have no text part, so set this to "" for string search in the next step
|
| 90 |
+
if last_user_message is None:
|
| 91 |
+
last_user_message = ""
|
| 92 |
+
|
| 93 |
+
# If a file was uploaded then SaveFilesAsArtifactsPlugin() adds "[Uploaded Artifact: file_name.csv]" to the user message
|
| 94 |
+
# Check for "Uploaded Artifact:" in the last user message
|
| 95 |
+
if "Uploaded Artifact:" in last_user_message:
|
| 96 |
+
|
| 97 |
+
# Add a text part only if there are any issues with accessing or saving the artifact
|
| 98 |
+
added_text = ""
|
| 99 |
+
# List available artifacts
|
| 100 |
+
artifacts = await callback_context.list_artifacts()
|
| 101 |
+
if len(artifacts) == 0:
|
| 102 |
+
added_text = "No uploaded file is available"
|
| 103 |
+
else:
|
| 104 |
+
most_recent_file = artifacts[-1]
|
| 105 |
+
try:
|
| 106 |
+
# Get artifact and byte data
|
| 107 |
+
artifact = await callback_context.load_artifact(
|
| 108 |
+
filename=most_recent_file
|
| 109 |
+
)
|
| 110 |
+
byte_data = artifact.inline_data.data
|
| 111 |
+
# Save artifact as temporary file
|
| 112 |
+
tmp_dir = "/tmp/uploads"
|
| 113 |
+
tmp_file_path = os.path.join(tmp_dir, most_recent_file)
|
| 114 |
+
# Write the file
|
| 115 |
+
with open(tmp_file_path, "wb") as f:
|
| 116 |
+
f.write(byte_data)
|
| 117 |
+
# Set appropriate permissions
|
| 118 |
+
os.chmod(tmp_file_path, 0o644)
|
| 119 |
+
print(f"[preprocess_artifact] Saved artifact as '{tmp_file_path}'")
|
| 120 |
+
|
| 121 |
+
except Exception as e:
|
| 122 |
+
added_text = f"Error processing artifact: {str(e)}"
|
| 123 |
+
|
| 124 |
+
# If there were any issues, add a new part to the user message
|
| 125 |
+
if added_text:
|
| 126 |
+
# llm_request.contents[-1].parts.append(types.Part(text=added_text))
|
| 127 |
+
llm_request.contents[0].parts.append(types.Part(text=added_text))
|
| 128 |
+
print(
|
| 129 |
+
f"[preprocess_artifact] Added text part to user message: '{added_text}'"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Return None to allow the possibly modified request to go to the LLM
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
async def preprocess_messages(
|
| 137 |
+
callback_context: CallbackContext, llm_request: LlmRequest
|
| 138 |
+
) -> Optional[LlmResponse]:
|
| 139 |
+
"""
|
| 140 |
+
Callback function to modify user messages to point to temporary artifact file paths.
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
# Changes to session state made by callbacks are not preserved across events
|
| 144 |
+
# See: https://github.com/google/adk-docs/issues/904
|
| 145 |
+
# Therefore, for every callback invocation we need to loop over all events, not just the most recent one
|
| 146 |
+
for i in range(len(llm_request.contents)):
|
| 147 |
+
# Inspect the user message in the request contents
|
| 148 |
+
user_message = llm_request.contents[i].parts[-1].text
|
| 149 |
+
if user_message:
|
| 150 |
+
# Modify file path in user message
|
| 151 |
+
# Original file path inserted by SaveFilesAsArtifactsPlugin():
|
| 152 |
+
# [Uploaded Artifact: "breast-cancer.csv"]
|
| 153 |
+
# Modified file path used by preprocess_artifact():
|
| 154 |
+
# [Uploaded File: "/tmp/uploads/breast-cancer.csv"]
|
| 155 |
+
tmp_dir = "/tmp/uploads/"
|
| 156 |
+
if '[Uploaded Artifact: "' in user_message:
|
| 157 |
+
user_message = user_message.replace(
|
| 158 |
+
'[Uploaded Artifact: "', f'[Uploaded File: "{tmp_dir}'
|
| 159 |
+
)
|
| 160 |
+
llm_request.contents[i].parts[-1].text = user_message
|
| 161 |
+
print(f"[preprocess_messages] Modified user message: '{user_message}'")
|
| 162 |
+
|
| 163 |
+
return None
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def detect_file_type(byte_data: bytes) -> Tuple[str, str]:
|
| 167 |
+
"""
|
| 168 |
+
Detect file type from magic number/bytes and return (mime_type, file_extension).
|
| 169 |
+
Supports BMP, JPEG, PNG, TIFF, and PDF.
|
| 170 |
+
"""
|
| 171 |
+
if len(byte_data) < 8:
|
| 172 |
+
# Default to PNG if we can't determine
|
| 173 |
+
return "image/png", "png"
|
| 174 |
+
|
| 175 |
+
# Check magic numbers
|
| 176 |
+
if byte_data.startswith(b"\x89PNG\r\n\x1a\n"):
|
| 177 |
+
return "image/png", "png"
|
| 178 |
+
elif byte_data.startswith(b"\xff\xd8\xff"):
|
| 179 |
+
return "image/jpeg", "jpg"
|
| 180 |
+
elif byte_data.startswith(b"BM"):
|
| 181 |
+
return "image/bmp", "bmp"
|
| 182 |
+
elif byte_data.startswith(b"II*\x00") or byte_data.startswith(b"MM\x00*"):
|
| 183 |
+
return "image/tiff", "tiff"
|
| 184 |
+
elif byte_data.startswith(b"%PDF"):
|
| 185 |
+
return "application/pdf", "pdf"
|
| 186 |
+
else:
|
| 187 |
+
# Default to PNG if we can't determine
|
| 188 |
+
return "image/png", "png"
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
async def skip_summarization_for_plot_success(
|
| 192 |
+
tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext, tool_response: Dict
|
| 193 |
+
) -> Optional[Dict]:
|
| 194 |
+
"""
|
| 195 |
+
Callback function to turn off summarization if plot succeeded.
|
| 196 |
+
"""
|
| 197 |
+
|
| 198 |
+
# If there was an error making the plot, the LLM tells the user what happened.
|
| 199 |
+
# This happens because skip_summarization is False by default.
|
| 200 |
+
|
| 201 |
+
# But if the plot was created successfully, there's
|
| 202 |
+
# no need for an extra LLM call to tell us it's there.
|
| 203 |
+
if tool.name in ["make_plot", "make_ggplot"]:
|
| 204 |
+
if not tool_response["isError"]:
|
| 205 |
+
tool_context.actions.skip_summarization = True
|
| 206 |
+
|
| 207 |
+
return None
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
async def save_plot_artifact(
|
| 211 |
+
tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext, tool_response: Dict
|
| 212 |
+
) -> Optional[Dict]:
|
| 213 |
+
"""
|
| 214 |
+
Callback function to save plot files as an ADK artifact.
|
| 215 |
+
"""
|
| 216 |
+
|
| 217 |
+
# Look for plot tool (so we don't bother with transfer_to_agent or other functions)
|
| 218 |
+
if tool.name in ["make_plot", "make_ggplot"]:
|
| 219 |
+
# In ADK 1.17.0, tool_response is a dict (i.e. result of model_dump method invoked on MCP CallToolResult instance):
|
| 220 |
+
# https://github.com/google/adk-python/commit/4df926388b6e9ebcf517fbacf2f5532fd73b0f71
|
| 221 |
+
# https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#parsing-tool-results
|
| 222 |
+
if "content" in tool_response and not tool_response["isError"]:
|
| 223 |
+
for content in tool_response["content"]:
|
| 224 |
+
if "type" in content and content["type"] == "text":
|
| 225 |
+
# Convert tool response (hex string) to bytes
|
| 226 |
+
byte_data = bytes.fromhex(content["text"])
|
| 227 |
+
|
| 228 |
+
# Detect file type from magic number
|
| 229 |
+
mime_type, file_extension = detect_file_type(byte_data)
|
| 230 |
+
|
| 231 |
+
# Encode binary data to Base64 format
|
| 232 |
+
encoded = base64.b64encode(byte_data).decode("utf-8")
|
| 233 |
+
artifact_part = types.Part(
|
| 234 |
+
inline_data={
|
| 235 |
+
"data": encoded,
|
| 236 |
+
"mime_type": mime_type,
|
| 237 |
+
}
|
| 238 |
+
)
|
| 239 |
+
# Use second part of tool name (e.g. make_ggplot -> ggplot.png)
|
| 240 |
+
filename = f"{tool.name.split("_", 1)[1]}.{file_extension}"
|
| 241 |
+
await tool_context.save_artifact(
|
| 242 |
+
filename=filename, artifact=artifact_part
|
| 243 |
+
)
|
| 244 |
+
# Format the success message as a tool response
|
| 245 |
+
text = f"Plot created and saved as an artifact: {filename}"
|
| 246 |
+
response = CallToolResult(
|
| 247 |
+
content=[TextContent(type="text", text=text)],
|
| 248 |
+
)
|
| 249 |
+
return response.model_dump(exclude_none=True, mode="json")
|
| 250 |
+
|
| 251 |
+
# Passthrough for other tools or no matching content (e.g. tool error)
|
| 252 |
+
return None
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
# Create agent to run R code
|
| 256 |
+
run_agent = LlmAgent(
|
| 257 |
+
name="Run",
|
| 258 |
+
description="Runs R code without making plots. Use the `Run` agent for executing code that does not load data or make a plot.",
|
| 259 |
+
model=model,
|
| 260 |
+
instruction=Run,
|
| 261 |
+
tools=[
|
| 262 |
+
McpToolset(
|
| 263 |
+
connection_params=connection_params,
|
| 264 |
+
tool_filter=["run_visible", "run_hidden"],
|
| 265 |
+
)
|
| 266 |
+
],
|
| 267 |
+
before_model_callback=[preprocess_artifact, preprocess_messages],
|
| 268 |
+
before_tool_callback=catch_tool_errors,
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
# Create agent to load data
|
| 272 |
+
data_agent = LlmAgent(
|
| 273 |
+
name="Data",
|
| 274 |
+
description="Loads data into an R data frame and summarizes it. Use the `Data` agent for loading data from a file or URL before making a plot.",
|
| 275 |
+
model=model,
|
| 276 |
+
instruction=Data,
|
| 277 |
+
tools=[
|
| 278 |
+
McpToolset(
|
| 279 |
+
connection_params=connection_params,
|
| 280 |
+
tool_filter=["run_visible"],
|
| 281 |
+
)
|
| 282 |
+
],
|
| 283 |
+
before_model_callback=[preprocess_artifact, preprocess_messages],
|
| 284 |
+
before_tool_callback=catch_tool_errors,
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# Create agent to make plots using R code
|
| 288 |
+
plot_agent = LlmAgent(
|
| 289 |
+
name="Plot",
|
| 290 |
+
description="Makes plots using R code. Use the `Plot` agent after loading any required data.",
|
| 291 |
+
model=model,
|
| 292 |
+
instruction=Plot,
|
| 293 |
+
tools=[
|
| 294 |
+
McpToolset(
|
| 295 |
+
connection_params=connection_params,
|
| 296 |
+
tool_filter=["make_plot", "make_ggplot"],
|
| 297 |
+
)
|
| 298 |
+
],
|
| 299 |
+
before_model_callback=[preprocess_artifact, preprocess_messages],
|
| 300 |
+
before_tool_callback=catch_tool_errors,
|
| 301 |
+
after_tool_callback=[skip_summarization_for_plot_success, save_plot_artifact],
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
# Create agent to install R packages
|
| 305 |
+
install_agent = LlmAgent(
|
| 306 |
+
name="Install",
|
| 307 |
+
description="Installs R packages. Use the `Install` agent when an R package needs to be installed.",
|
| 308 |
+
model=model,
|
| 309 |
+
instruction=Install,
|
| 310 |
+
tools=[
|
| 311 |
+
McpToolset(
|
| 312 |
+
connection_params=connection_params,
|
| 313 |
+
tool_filter=["run_visible"],
|
| 314 |
+
)
|
| 315 |
+
],
|
| 316 |
+
before_model_callback=[preprocess_artifact, preprocess_messages],
|
| 317 |
+
before_tool_callback=catch_tool_errors,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
# Create parent agent and assign children via sub_agents
|
| 321 |
+
root_agent = LlmAgent(
|
| 322 |
+
name="Coordinator",
|
| 323 |
+
# "Use the..." tells sub-agents to transfer to Coordinator for help requests
|
| 324 |
+
description="Multi-agent system for performing actions in R. Use the `Coordinator` agent for getting help on packages, datasets, and functions.",
|
| 325 |
+
model=model,
|
| 326 |
+
instruction=Root,
|
| 327 |
+
# To pass control back to root, the help and run functions should be tools or a ToolAgent (not sub_agent)
|
| 328 |
+
tools=[
|
| 329 |
+
McpToolset(
|
| 330 |
+
connection_params=connection_params,
|
| 331 |
+
tool_filter=["help_package", "help_topic"],
|
| 332 |
+
)
|
| 333 |
+
],
|
| 334 |
+
sub_agents=[
|
| 335 |
+
run_agent,
|
| 336 |
+
data_agent,
|
| 337 |
+
plot_agent,
|
| 338 |
+
install_agent,
|
| 339 |
+
],
|
| 340 |
+
# Select R session
|
| 341 |
+
before_agent_callback=select_r_session,
|
| 342 |
+
# Save user-uploaded artifact as a temporary file and modify messages to point to this file
|
| 343 |
+
before_model_callback=[preprocess_artifact, preprocess_messages],
|
| 344 |
+
before_tool_callback=catch_tool_errors,
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
app = App(
|
| 348 |
+
name="PlotMyData",
|
| 349 |
+
root_agent=root_agent,
|
| 350 |
+
# This inserts user messages like '[Uploaded Artifact: "breast-cancer.csv"]'
|
| 351 |
+
plugins=[SaveFilesAsArtifactsPlugin()],
|
| 352 |
+
)
|
README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
short_description: Data analysis and plotting with Google ADK, MCP, and R
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: PlotMyData
|
| 3 |
+
emoji: 👀
|
| 4 |
+
colorFrom: yellow
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
short_description: Data analysis and plotting with Google ADK, MCP, and R
|
| 10 |
+
app_port: 8080
|
| 11 |
---
|
| 12 |
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
entrypoint.sh
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
|
| 3 |
+
# Exit immediately on errors
|
| 4 |
+
set -e
|
| 5 |
+
|
| 6 |
+
# Use profile for persistent R session
|
| 7 |
+
cp profile.R .Rprofile
|
| 8 |
+
|
| 9 |
+
# Start R in a detached screen session
|
| 10 |
+
# TODO: Look at using supervisord for another way to run multiple services
|
| 11 |
+
# https://docs.docker.com/engine/containers/multi-service_container/#use-a-process-manager
|
| 12 |
+
screen -d -m R
|
| 13 |
+
|
| 14 |
+
# Activate virtual environment
|
| 15 |
+
export PATH="/opt/venv/bin:$PATH"
|
| 16 |
+
|
| 17 |
+
# Set OpenAI model
|
| 18 |
+
export OPENAI_MODEL_NAME=gpt-4o
|
| 19 |
+
echo "Using OpenAI with ${OPENAI_MODEL_NAME}"
|
| 20 |
+
|
| 21 |
+
# Suppress e.g. UserWarning: [EXPERIMENTAL] BaseAuthenticatedTool: This feature is experimental ...
|
| 22 |
+
# https://github.com/google/adk-python/commit/4afc9b2f33d63381583cea328f97c02213611529
|
| 23 |
+
export ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS=true
|
| 24 |
+
|
| 25 |
+
# For local development, the API key is read from a file
|
| 26 |
+
# (not needed on HF Spaces, where secrets are injected into container's environment)
|
| 27 |
+
if [ -z "$OPENAI_API_KEY" ]; then
|
| 28 |
+
export OPENAI_API_KEY=$(cat /run/secrets/openai-api-key)
|
| 29 |
+
fi
|
| 30 |
+
|
| 31 |
+
exec adk web --host 0.0.0.0 --port 8080 --reload_agents
|
functions.R
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Summarize a data frame, for example:
|
| 2 |
+
# Data frame dimensions: 10 rows x 3 columns
|
| 3 |
+
# Data Summary:
|
| 4 |
+
# col1: integer
|
| 5 |
+
# col2: numeric, missing=3
|
| 6 |
+
# col3: character
|
| 7 |
+
data_summary <- function(df) {
|
| 8 |
+
nrows <- nrow(df)
|
| 9 |
+
ncols <- ncol(df)
|
| 10 |
+
lines <- c(sprintf("Data frame dimensions: %d rows x %d columns", nrows, ncols), "Data Summary:")
|
| 11 |
+
|
| 12 |
+
# Helper for R data type names
|
| 13 |
+
type_map <- function(x) {
|
| 14 |
+
if (is.factor(x)) return("factor")
|
| 15 |
+
if (is.character(x)) return("character")
|
| 16 |
+
if (is.logical(x)) return("logical")
|
| 17 |
+
if (inherits(x, "Date")) return("Date")
|
| 18 |
+
if (is.numeric(x)) {
|
| 19 |
+
vals <- x[!is.na(x)]
|
| 20 |
+
if (length(vals) > 0 && all(abs(vals - round(vals)) < .Machine$double.eps^0.5)) return("integer")
|
| 21 |
+
return("numeric")
|
| 22 |
+
}
|
| 23 |
+
return(class(x)[1])
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
for (col in names(df)) {
|
| 27 |
+
dtype <- type_map(df[[col]])
|
| 28 |
+
miss <- sum(is.na(df[[col]]))
|
| 29 |
+
if (miss > 0) {
|
| 30 |
+
lines <- c(lines, sprintf("%s: %s, missing=%d", col, dtype, miss))
|
| 31 |
+
} else {
|
| 32 |
+
lines <- c(lines, sprintf("%s: %s", col, dtype))
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
paste(lines, collapse = "\n")
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# Check if packages are installed and return status message
|
| 39 |
+
# Example: check_packages(c("nlme", "ggplot2", "scatterplot3d"))
|
| 40 |
+
# Returns: "nlme and ggplot2 are already installed" if all are installed
|
| 41 |
+
# Or: "scatterplot3d needs to be installed" if some are missing
|
| 42 |
+
# The message format makes it easy to determine if installation is needed:
|
| 43 |
+
# - If message contains "are already installed" and does NOT contain "needs to be installed", all packages are installed
|
| 44 |
+
# - If message contains "needs to be installed", some packages need installation
|
| 45 |
+
check_packages <- function(packages) {
|
| 46 |
+
if (length(packages) == 0) {
|
| 47 |
+
return("No packages specified")
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
# Check which packages are installed
|
| 51 |
+
installed <- sapply(packages, function(pkg) {
|
| 52 |
+
requireNamespace(pkg, quietly = TRUE)
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
installed_pkgs <- packages[installed]
|
| 56 |
+
missing_pkgs <- packages[!installed]
|
| 57 |
+
|
| 58 |
+
if (length(installed_pkgs) == length(packages)) {
|
| 59 |
+
# All packages are installed
|
| 60 |
+
if (length(installed_pkgs) == 1) {
|
| 61 |
+
return(paste(installed_pkgs, "is already installed"))
|
| 62 |
+
} else if (length(installed_pkgs) == 2) {
|
| 63 |
+
return(paste(installed_pkgs[1], "and", installed_pkgs[2], "are already installed"))
|
| 64 |
+
} else {
|
| 65 |
+
# Format: "pkg1, pkg2, and pkg3 are already installed"
|
| 66 |
+
pkgs_list <- paste(installed_pkgs[-length(installed_pkgs)], collapse = ", ")
|
| 67 |
+
return(paste(pkgs_list, "and", installed_pkgs[length(installed_pkgs)], "are already installed"))
|
| 68 |
+
}
|
| 69 |
+
} else if (length(installed_pkgs) > 0) {
|
| 70 |
+
# Some packages are installed, some are missing
|
| 71 |
+
if (length(installed_pkgs) == 1) {
|
| 72 |
+
installed_msg <- paste(installed_pkgs, "is already installed")
|
| 73 |
+
} else if (length(installed_pkgs) == 2) {
|
| 74 |
+
installed_msg <- paste(installed_pkgs[1], "and", installed_pkgs[2], "are already installed")
|
| 75 |
+
} else {
|
| 76 |
+
pkgs_list <- paste(installed_pkgs[-length(installed_pkgs)], collapse = ", ")
|
| 77 |
+
installed_msg <- paste(pkgs_list, "and", installed_pkgs[length(installed_pkgs)], "are already installed")
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if (length(missing_pkgs) == 1) {
|
| 81 |
+
missing_msg <- paste(missing_pkgs, "needs to be installed")
|
| 82 |
+
} else if (length(missing_pkgs) == 2) {
|
| 83 |
+
missing_msg <- paste(missing_pkgs[1], "and", missing_pkgs[2], "need to be installed")
|
| 84 |
+
} else {
|
| 85 |
+
pkgs_list <- paste(missing_pkgs[-length(missing_pkgs)], collapse = ", ")
|
| 86 |
+
missing_msg <- paste(pkgs_list, "and", missing_pkgs[length(missing_pkgs)], "need to be installed")
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
return(paste(installed_msg, ";", missing_msg))
|
| 90 |
+
} else {
|
| 91 |
+
# No packages are installed
|
| 92 |
+
if (length(missing_pkgs) == 1) {
|
| 93 |
+
return(paste(missing_pkgs, "needs to be installed"))
|
| 94 |
+
} else if (length(missing_pkgs) == 2) {
|
| 95 |
+
return(paste(missing_pkgs[1], "and", missing_pkgs[2], "need to be installed"))
|
| 96 |
+
} else {
|
| 97 |
+
pkgs_list <- paste(missing_pkgs[-length(missing_pkgs)], collapse = ", ")
|
| 98 |
+
return(paste(pkgs_list, "and", missing_pkgs[length(missing_pkgs)], "need to be installed"))
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
profile.R
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Set a default CRAN mirror
|
| 2 |
+
options(repos = c(CRAN = "https://cloud.r-project.org"))
|
| 3 |
+
|
| 4 |
+
# Load a commonly used package
|
| 5 |
+
library(tidyverse)
|
| 6 |
+
|
| 7 |
+
# Use our own data summary function
|
| 8 |
+
source("functions.R")
|
| 9 |
+
|
| 10 |
+
# Make this R session visible to the mcptools MCP server
|
| 11 |
+
# NOTE: mcp_session() needs to be run in an *interactive* R session, so we can't put it in server.R
|
| 12 |
+
mcptools::mcp_session()
|
prompts.R
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
make_plot_prompt <- '
|
| 2 |
+
Runs R code to make a plot with base R graphics.
|
| 3 |
+
|
| 4 |
+
Args:
|
| 5 |
+
code: R code to run
|
| 6 |
+
|
| 7 |
+
Returns:
|
| 8 |
+
Binary image data
|
| 9 |
+
|
| 10 |
+
Details:
|
| 11 |
+
`code` should be R code that begins with e.g. `png(filename)` and ends with `dev.off()`.
|
| 12 |
+
Always use the variable `filename` instead of an actual file name.
|
| 13 |
+
|
| 14 |
+
Example: User requests "Plot x (1,2,3) and y (10,20,30)", then `code` is:
|
| 15 |
+
|
| 16 |
+
png(filename)
|
| 17 |
+
x <- c(1, 2, 3)
|
| 18 |
+
y <- c(10, 20, 30)
|
| 19 |
+
plot(x, y)
|
| 20 |
+
dev.off()
|
| 21 |
+
|
| 22 |
+
Example: User requests "Give me a 8.5x11 inch PDF of y = x^2 from -1 to 1, large font, titled with the function", then `code` is:
|
| 23 |
+
|
| 24 |
+
pdf(filename, width = 8.5, height = 11)
|
| 25 |
+
par(cex = 2)
|
| 26 |
+
x <- seq(-1, 1, length.out = 100)
|
| 27 |
+
y <- x^2
|
| 28 |
+
plot(x, y, type = "l")
|
| 29 |
+
title(main = quote(y == x^2))
|
| 30 |
+
dev.off()
|
| 31 |
+
|
| 32 |
+
Example: User requests "Plot radius_worst (y) vs radius_mean (x) from https://zenodo.org/records/3608984/files/breastcancer.csv?download=1", then `code` is:
|
| 33 |
+
|
| 34 |
+
png(filename)
|
| 35 |
+
df <- read.csv("https://zenodo.org/records/3608984/files/breastcancer.csv?download=1")
|
| 36 |
+
plot(df$radius_mean, df$radius_worst, xlab = "radius_worst", ylab = "radius_mean")
|
| 37 |
+
dev.off()
|
| 38 |
+
|
| 39 |
+
Example: User requests "Plot radius_worst (y) vs radius_mean (x)" and [Uploaded File: "/tmp/uploads/breast-cancer.csv"], then `code` is:
|
| 40 |
+
|
| 41 |
+
png(filename)
|
| 42 |
+
df <- read.csv("/tmp/uploads/breast-cancer.csv")
|
| 43 |
+
plot(df$radius_mean, df$radius_worst, xlab = "radius_worst", ylab = "radius_mean")
|
| 44 |
+
dev.off()
|
| 45 |
+
'
|
| 46 |
+
|
| 47 |
+
make_ggplot_prompt <- '
|
| 48 |
+
Runs R code to make a plot with ggplot/ggplot2.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
code: R code to run
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
Binary image data
|
| 55 |
+
|
| 56 |
+
Details:
|
| 57 |
+
`code` should be R code that begins with `library(ggplot2)` and ends with `ggsave(filename, device = "png")`.
|
| 58 |
+
|
| 59 |
+
Example: User requests "ggplot wt vs mpg from mtcars", then `code` is:
|
| 60 |
+
|
| 61 |
+
library(ggplot2)
|
| 62 |
+
ggplot(mtcars, aes(mpg, wt)) +
|
| 63 |
+
geom_point()
|
| 64 |
+
ggsave(filename, device = "png")
|
| 65 |
+
|
| 66 |
+
Example: User requests "ggplot wt vs mpg from mtcars as pdf", then `code` is:
|
| 67 |
+
|
| 68 |
+
library(ggplot2)
|
| 69 |
+
ggplot(mtcars, aes(mpg, wt)) +
|
| 70 |
+
geom_point()
|
| 71 |
+
ggsave(filename, device = "pdf")
|
| 72 |
+
|
| 73 |
+
Important notes:
|
| 74 |
+
|
| 75 |
+
- `code` must end with ggsave(filename, device = ) with a specified device.
|
| 76 |
+
- Use `device = "png"` unless the user requests a different format.
|
| 77 |
+
- Always use the variable `filename` instead of an actual file name.
|
| 78 |
+
'
|
| 79 |
+
|
| 80 |
+
help_topic_prompt <- '
|
| 81 |
+
Gets documentation for a dataset, function, or other topic.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
topic: Topic or function to get help for.
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
Documentation text. May include runnable R examples.
|
| 88 |
+
|
| 89 |
+
Examples:
|
| 90 |
+
- Show the arguments of the `lm` function: help_topic("lm").
|
| 91 |
+
- Show the format of the `airquality` dataset: help_topic("airquality").
|
| 92 |
+
- Get variables in `Titanic`: help_topic("Titanic").
|
| 93 |
+
'
|
| 94 |
+
|
| 95 |
+
help_package_prompt <- '
|
| 96 |
+
Summarizes datasets and functions in an R package.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
package: Package to get help for.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Documentation text. Includes a package description and index of functions and datasets.
|
| 103 |
+
|
| 104 |
+
Examples:
|
| 105 |
+
- Get the names of R datasets: help_package("datasets").
|
| 106 |
+
- List graphics functions in base R: help_package("graphics").
|
| 107 |
+
'
|
| 108 |
+
|
| 109 |
+
run_visible_prompt <- '
|
| 110 |
+
Runs R code and returns the result.
|
| 111 |
+
Does not make plots.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
code: R code to run.
|
| 115 |
+
|
| 116 |
+
Returns:
|
| 117 |
+
Result of R code execution.
|
| 118 |
+
'
|
| 119 |
+
|
| 120 |
+
run_hidden_prompt <- '
|
| 121 |
+
Run R code without returning the result.
|
| 122 |
+
Does not make plots.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
code: R code to run.
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
Nothing.
|
| 129 |
+
|
| 130 |
+
NOTE: Choose this tool if:
|
| 131 |
+
- The user asks to save the result in a variable, or
|
| 132 |
+
- You are performing intermediate calculations before making a plot.
|
| 133 |
+
'
|
prompts.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Root = """
|
| 2 |
+
Your purpose is to interact with an R session to perform data analysis and visualization on the user's behalf.
|
| 3 |
+
You cannot run code directly, but may use the `Data`, `Plot`, `Run`, and `Install` agents.
|
| 4 |
+
|
| 5 |
+
Only use the `Run` agent if the following conditions are both true:
|
| 6 |
+
|
| 7 |
+
- The operation is requested by the user ("calculate" or "run"), and
|
| 8 |
+
- The code does not make a plot, chart, graph, or any other visualization.
|
| 9 |
+
|
| 10 |
+
You may call a help tool before transfering control to an agent:
|
| 11 |
+
|
| 12 |
+
- If an R dataset ("dataset") is requested, use help_package('datasets') to find the correct dataset name.
|
| 13 |
+
- If the user requests documentation for specific datasets or functions, use the `help_topic` tool.
|
| 14 |
+
|
| 15 |
+
Examples:
|
| 16 |
+
|
| 17 |
+
- Query includes "?boxplot": The user is requesting documentation. Call help_topic('boxplot') then transfer to an agent.
|
| 18 |
+
- "Plot distance vs speed from the cars dataset": This is a plot request using an R dataset. Call help_package('datasets') then transfer to the `Data` agent.
|
| 19 |
+
- "Calculate x = cos(x) for x = 0 to 12 and make a plot": This is a plot that does not require data. Transfer to the `Plot` agent.
|
| 20 |
+
- "Run x <- 2": This is code execution without data or plot. Transfer to the `Run` agent.
|
| 21 |
+
- "Load the data": The user is asking to load data from an uploaded file. Transfer to the `Data` agent.
|
| 22 |
+
|
| 23 |
+
Important notes:
|
| 24 |
+
|
| 25 |
+
- Data may be provided directly by the user, in a URL, in an "Uploaded File" message, or an R dataset.
|
| 26 |
+
- You must not use the `Run` agent to make a plot or execute any other plotting commands.
|
| 27 |
+
- The only way to make a plot, chart, graph, or other visualization is to transfer to the `Data` or `Plot` agents.
|
| 28 |
+
- If an R package needs to be installed, transfer to the `Install` agent. Do not use install.packages(), library(), or any other commands for package installation and loading.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
Run = """
|
| 32 |
+
You are an agent that runs R code using the `run_visible` and `run_hidden` tools.
|
| 33 |
+
You cannot make plots.
|
| 34 |
+
|
| 35 |
+
Perform the following actions:
|
| 36 |
+
- Interpret the user's request as R code.
|
| 37 |
+
- If the code makes a plot (including ggplot or any other type of graph or visualization), transfer to the `Plot` agent.
|
| 38 |
+
- If the code assigns the result to a variable, pass the code to the `run_hidden` tool.
|
| 39 |
+
- Otherwise, pass the code to the `run_visible` tool.
|
| 40 |
+
|
| 41 |
+
Important notes:
|
| 42 |
+
|
| 43 |
+
- The `run_hidden` tool runs R commands without returning the result. This is useful for reducing LLM token usage while working with large variables.
|
| 44 |
+
- You can use dplyr, tidyr, and other tidyverse packages.
|
| 45 |
+
- Your response should always be valid, self-contained R code.
|
| 46 |
+
- If the tool response is an error (isError: true), respond with the exact text of the error message and stop running code.
|
| 47 |
+
- If you need an R package that is not installed, transfer to the `Install` agent to install it, then transfer back to continue running the code.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
Data = """
|
| 51 |
+
You are an agent that loads and summarizes data.
|
| 52 |
+
Your main task has three parts:
|
| 53 |
+
|
| 54 |
+
1. Generate R code to create a `df` object and summarize it with `data_summary(df)`.
|
| 55 |
+
2. Use the `run_visible` tool to execute the code.
|
| 56 |
+
3. Transfer to the `Plot` agent to make a plot.
|
| 57 |
+
|
| 58 |
+
Choose the first available data source:
|
| 59 |
+
|
| 60 |
+
1: Data provided directly by the user.
|
| 61 |
+
2: File provided in an "Uploaded File" message. Do not use other files.
|
| 62 |
+
3: URL provided by the user. Do not use other URLs.
|
| 63 |
+
4: Available R dataset that matches the user's request.
|
| 64 |
+
|
| 65 |
+
Examples of code for `run_visible`:
|
| 66 |
+
|
| 67 |
+
- User requests "plot 1,2,3 10,20,30": code is `df <- data.frame(x = c(1,2,3), y = (10, 20, 30))
|
| 68 |
+
data_summary(df)`.
|
| 69 |
+
- User requests "plot cars data": code is `df <- data.frame(cars)
|
| 70 |
+
data_summary(df)`
|
| 71 |
+
- To read CSV data from a URL, use `df <- read.csv(csv_url)`, where csv_url is the exact URL provided by the user.
|
| 72 |
+
- To read CSV data from a file, use `df <- read.csv(file_path)`, where file_path is provided in an "Uploaded File" user message.
|
| 73 |
+
|
| 74 |
+
What to do after calling `run_visible`:
|
| 75 |
+
|
| 76 |
+
- If "Data Summary" exists and the user requested a plot, then pass control to the `Plot` agent.
|
| 77 |
+
- If "Data Summary" exists and the user did not request a plot, then stop the workflow.
|
| 78 |
+
- If the user provided data but "Data Summary" does not exist, then stop and report a problem.
|
| 79 |
+
|
| 80 |
+
Important notes:
|
| 81 |
+
|
| 82 |
+
- Do not use the `run_visible` tool to make a plot.
|
| 83 |
+
- Run `data_summary(df)` in your code. Do not run `summary(df)`.
|
| 84 |
+
- You can use dplyr, tidyr, and other tidyverse packages.
|
| 85 |
+
- If you need an R package that is not installed, transfer to the `Install` agent to install it, then transfer back to continue loading the data.
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
Plot = """
|
| 89 |
+
You are an agent that makes plots with R code using the `make_plot` and `make_ggplot` tools.
|
| 90 |
+
|
| 91 |
+
Coding strategy:
|
| 92 |
+
|
| 93 |
+
- Use previously assigned variables (especially `df`) in your code.
|
| 94 |
+
- Do not load data yourself.
|
| 95 |
+
- Use a specific variable other than `df` if it is better for making the plot.
|
| 96 |
+
- Choose column names in `df` based on the user's request.
|
| 97 |
+
- Column names are case-sensitive, syntactically valid R names.
|
| 98 |
+
- Look in the Data Summary for details.
|
| 99 |
+
- No data are required for plotting functions and simulations.
|
| 100 |
+
|
| 101 |
+
Plot tools:
|
| 102 |
+
|
| 103 |
+
- For base R graphics use the `make_plot` tool.
|
| 104 |
+
- For ggplot/ggplot2 use the `make_ggplot` tool.
|
| 105 |
+
- Both of these tools save the plot as a conversation artifact that is visible to the user.
|
| 106 |
+
|
| 107 |
+
Examples:
|
| 108 |
+
- User requests to plot "dates", but the Data Summary lists a "Date" column. Answer: use `df$Date`.
|
| 109 |
+
- User requests to plot "volcano", but `df` also exists. Answer: The `volcano` matrix is better for images; use `image(volcano)`.
|
| 110 |
+
|
| 111 |
+
Important notes:
|
| 112 |
+
|
| 113 |
+
- Use base R graphics unless the user asks for ggplot or ggplot2.
|
| 114 |
+
- Pay attention to the user's request and use your knowledge of R to write code that gives the best-looking plot.
|
| 115 |
+
- Your response should always be valid, self-contained R code.
|
| 116 |
+
- If you need an R package that is not installed, transfer to the `Install` agent to install it, then transfer back to continue making the plot.
|
| 117 |
+
"""
|
| 118 |
+
|
| 119 |
+
Install = """
|
| 120 |
+
You are an agent that installs R packages using the `run_visible` tool.
|
| 121 |
+
|
| 122 |
+
Your workflow:
|
| 123 |
+
|
| 124 |
+
1. Identify which packages need to be installed.
|
| 125 |
+
2. First, check package installation status by calling `check_packages()` function using the `run_visible` tool. For example: `check_packages(c("package1", "package2"))`.
|
| 126 |
+
3. Examine the result from `check_packages()`:
|
| 127 |
+
- If the result indicates all packages are already installed (contains "are already installed" and does NOT contain "needs to be installed"), then immediately transfer control back to the agent that requested the installation WITHOUT asking for confirmation.
|
| 128 |
+
- If the result indicates some or all packages need to be installed (contains "needs to be installed"), proceed to step 4.
|
| 129 |
+
4. Clearly state which packages you will install (e.g., "I need to install the following packages: scatterplot3d, plotly").
|
| 130 |
+
5. Ask the user for confirmation before proceeding (e.g., "Should I proceed with installing these packages?").
|
| 131 |
+
6. Wait for the user to confirm before installing.
|
| 132 |
+
7. Once confirmed, use the `run_visible` tool with R code like: `install.packages(c("package1", "package2"))` to install only the packages that are missing.
|
| 133 |
+
8. After successful installation, transfer control back to the agent that requested the installation (e.g., transfer to the `Plot` agent if it was making a plot).
|
| 134 |
+
|
| 135 |
+
Important notes:
|
| 136 |
+
|
| 137 |
+
- ALWAYS call `check_packages()` first to check installation status before attempting to install.
|
| 138 |
+
- If all packages are already installed, return to the previous agent immediately without asking for confirmation.
|
| 139 |
+
- Only ask for user confirmation if some packages actually need to be installed.
|
| 140 |
+
- ALWAYS clearly state which packages will be installed.
|
| 141 |
+
- Use `run_visible` with `install.packages()` to install packages.
|
| 142 |
+
- For multiple packages, use: `install.packages(c("package1", "package2"))`.
|
| 143 |
+
- For a single package, use: `install.packages("package1")`.
|
| 144 |
+
- If installation fails, report the error to the user and do not transfer control.
|
| 145 |
+
- If installation succeeds, transfer control back to the calling agent to continue the original task.
|
| 146 |
+
- Do not install packages without explicit user confirmation (unless all packages are already installed).
|
| 147 |
+
"""
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
google-adk==1.23.0
|
| 2 |
+
litellm==1.80.13
|
| 3 |
+
mcp==1.26.0
|
server.R
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 20251009 Added plot tool
|
| 2 |
+
# 20251023 Added help tools
|
| 3 |
+
|
| 4 |
+
# Load ellmer for tool() and type_*()
|
| 5 |
+
library(ellmer)
|
| 6 |
+
|
| 7 |
+
# Read prompts
|
| 8 |
+
source("prompts.R")
|
| 9 |
+
|
| 10 |
+
# Get help for a package
|
| 11 |
+
help_package <- function(package) {
|
| 12 |
+
help_page <- help(package = (package), help_type = "text")
|
| 13 |
+
paste(unlist(help_page$info), collapse = "\n")
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
# Get help for a topic
|
| 17 |
+
# Adapted from https://github.com/posit-dev/btw:::help_to_rd
|
| 18 |
+
help_topic <- function(topic) {
|
| 19 |
+
help_page <- help(topic = (topic), help_type = "text")
|
| 20 |
+
if(length(help_page) == 0) {
|
| 21 |
+
return(paste0("No help found for '", topic, "'. Please check the name and try again."))
|
| 22 |
+
}
|
| 23 |
+
# Handle multiple help files for a topic
|
| 24 |
+
# e.g. help_topic(plot) returns the help for both base::plot and graphics::plot.default
|
| 25 |
+
help_paths <- as.character(help_page)
|
| 26 |
+
help_result <- sapply(help_paths, function(help_path) {
|
| 27 |
+
rd_name <- basename(help_path)
|
| 28 |
+
rd_package <- basename(dirname(dirname(help_path)))
|
| 29 |
+
db <- tools::Rd_db(rd_package)[[paste0(rd_name, ".Rd")]]
|
| 30 |
+
paste(as.character(db), collapse = "")
|
| 31 |
+
})
|
| 32 |
+
# Insert headings to help the LLM distinguish multiple help files
|
| 33 |
+
# Heading before each help file (e.g. Help file 1, Help file 2)
|
| 34 |
+
help_result <- paste0("## Help file ", seq_along(help_result), ":\n", help_result)
|
| 35 |
+
# Heading at start of message (e.g. 2 help files were retrieved)
|
| 36 |
+
if(length(help_paths) == 1) help_info <- paste0("# ", length(help_paths), " help file was retrieved: ", paste(help_paths, collapse = ", "), ":\n")
|
| 37 |
+
if(length(help_paths) > 1) help_info <- paste0("# ", length(help_paths), " help files were retrieved: ", paste(help_paths, collapse = ", "), ":\n")
|
| 38 |
+
help_result <- c(help_info, help_result)
|
| 39 |
+
help_result
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
# Run R code and return the result
|
| 43 |
+
# https://github.com/posit-dev/mcptools/issues/71
|
| 44 |
+
run_visible <- function(code) {
|
| 45 |
+
eval(parse(text = code), globalenv())
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# Run R code without returning the result
|
| 49 |
+
# https://github.com/posit-dev/mcptools/issues/71
|
| 50 |
+
run_hidden <- function(code) {
|
| 51 |
+
eval(parse(text = code), globalenv())
|
| 52 |
+
return("The code executed successfully")
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
# Run R code to make a plot and return the image data
|
| 56 |
+
make_plot <- function(code) {
|
| 57 |
+
# Cursor, Bing and Google AI all suggest this but it causes an error:
|
| 58 |
+
# Error in png(filename = raw_conn) :
|
| 59 |
+
# 'filename' must be a non-empty character string
|
| 60 |
+
## Write plot to an in-memory PNG
|
| 61 |
+
#raw_conn <- rawConnection(raw(), open = "wb")
|
| 62 |
+
#png(filename = raw_conn)
|
| 63 |
+
|
| 64 |
+
# Use a temporary file to save the plot
|
| 65 |
+
filename <- tempfile(fileext = ".dat")
|
| 66 |
+
on.exit(unlink(filename))
|
| 67 |
+
|
| 68 |
+
# Run the plotting code (this should include e.g. png() and dev.off())
|
| 69 |
+
# The code uses a local variable (filename), so don't use envir = globalenv() here
|
| 70 |
+
eval(parse(text = code))
|
| 71 |
+
|
| 72 |
+
# Return a PNG image as raw bytes so ADK can save it as an artifact
|
| 73 |
+
readr::read_file_raw(filename)
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
# This is the same code as make_plot() but has a different tool description
|
| 77 |
+
make_ggplot <- function(code) {
|
| 78 |
+
filename <- tempfile(fileext = ".dat")
|
| 79 |
+
on.exit(unlink(filename))
|
| 80 |
+
eval(parse(text = code))
|
| 81 |
+
readr::read_file_raw(filename)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
mcptools::mcp_server(tools = list(
|
| 85 |
+
|
| 86 |
+
tool(
|
| 87 |
+
help_package,
|
| 88 |
+
help_package_prompt,
|
| 89 |
+
arguments = list(
|
| 90 |
+
package = type_string("Package to get help for.")
|
| 91 |
+
)
|
| 92 |
+
),
|
| 93 |
+
|
| 94 |
+
tool(
|
| 95 |
+
help_topic,
|
| 96 |
+
help_topic_prompt,
|
| 97 |
+
arguments = list(
|
| 98 |
+
topic = type_string("Topic or function to get help for.")
|
| 99 |
+
)
|
| 100 |
+
),
|
| 101 |
+
|
| 102 |
+
tool(
|
| 103 |
+
run_visible,
|
| 104 |
+
run_visible_prompt,
|
| 105 |
+
arguments = list(
|
| 106 |
+
code = type_string("R code to run.")
|
| 107 |
+
)
|
| 108 |
+
),
|
| 109 |
+
|
| 110 |
+
tool(
|
| 111 |
+
run_hidden,
|
| 112 |
+
run_hidden_prompt,
|
| 113 |
+
arguments = list(
|
| 114 |
+
code = type_string("R code to run.")
|
| 115 |
+
)
|
| 116 |
+
),
|
| 117 |
+
|
| 118 |
+
tool(
|
| 119 |
+
make_plot,
|
| 120 |
+
make_plot_prompt,
|
| 121 |
+
arguments = list(
|
| 122 |
+
code = type_string("R code to make the plot.")
|
| 123 |
+
)
|
| 124 |
+
),
|
| 125 |
+
|
| 126 |
+
tool(
|
| 127 |
+
make_ggplot,
|
| 128 |
+
make_ggplot_prompt,
|
| 129 |
+
arguments = list(
|
| 130 |
+
code = type_string("R code to make the plot.")
|
| 131 |
+
)
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
))
|