From 8dda42906c3735cbb4a62e292e2b9ecef5623e37 Mon Sep 17 00:00:00 2001 From: Mateusz Szewczyk Date: Tue, 2 Jun 2026 14:31:06 +0200 Subject: [PATCH 01/12] chore: Added AutoAI gneric template --- .../mcp_server/requirements.txt | 5 + .../mcp_server/server.py | 43 ++++ .../mcp_server/utils.py | 99 +++++++++ .../scripts/generate_template.py | 206 ++++++++++++++++++ .../template.env | 5 + 5 files changed, 358 insertions(+) create mode 100644 agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/requirements.txt create mode 100644 agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/server.py create mode 100644 agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/utils.py create mode 100644 agents/community/mcp-orchestrate-autoai-template-generic/scripts/generate_template.py create mode 100644 agents/community/mcp-orchestrate-autoai-template-generic/template.env diff --git a/agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/requirements.txt b/agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/requirements.txt new file mode 100644 index 0000000..b626e2e --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/requirements.txt @@ -0,0 +1,5 @@ +mcp +python-dotenv +ibm-watsonx-ai +pydantic +pyyaml \ No newline at end of file diff --git a/agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/server.py b/agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/server.py new file mode 100644 index 0000000..9572c48 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/server.py @@ -0,0 +1,43 @@ +from mcp.server.fastmcp import FastMCP + +from utils import ( + build_scoring_payload, + create_input_model, + extract_prediction, + get_autoai_deployment_id, + get_prediction_column, + get_server_name, + get_tool_name, + prepare_api_client, +) + +mcp = FastMCP(get_server_name()) +AutoAIInput = create_input_model() + + +def _build_tool_description() -> str: + prediction_column = get_prediction_column() + + return ( + f"Predict the value of '{prediction_column}' using the deployed watsonx.ai AutoAI model.\n\n" + "Provide all required input fields defined by the generated schema.\n\n" + "Returns the prediction result from the deployment." + ) + + +@mcp.tool(name=get_tool_name(), description=_build_tool_description()) +def invoke_autoai_prediction(input_data: AutoAIInput): + api_client = prepare_api_client() + deployment_id = get_autoai_deployment_id() + prediction_column = get_prediction_column() + + payload = build_scoring_payload(input_data) + response = api_client.deployments.score(deployment_id, meta_props=payload) + return { + "prediction_column": prediction_column, + "prediction": extract_prediction(response), + } + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/utils.py b/agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/utils.py new file mode 100644 index 0000000..15a65eb --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-template-generic/mcp_server/utils.py @@ -0,0 +1,99 @@ +import os +from typing import Any + +from dotenv import load_dotenv +from ibm_watsonx_ai import APIClient, Credentials +from pydantic import BaseModel, Field, create_model + +from generated_config import INPUT_FIELDS, PREDICTION_COLUMN, SERVER_NAME, TOOL_NAME + + +def load_env() -> None: + load_dotenv() + + +def prepare_api_client() -> APIClient: + load_env() + return APIClient( + credentials=Credentials( + url=os.getenv("WATSONX_URL"), + api_key=os.getenv("WATSONX_API_KEY"), + ), + space_id=os.getenv("WATSONX_SPACE_ID"), + ) + + +def get_autoai_deployment_id() -> str: + load_env() + deployment_id = os.getenv("WATSONX_AUTOAI_DEPLOYMENT_ID") + if not deployment_id: + raise ValueError("WATSONX_AUTOAI_DEPLOYMENT_ID is not set") + return deployment_id + + +def get_server_name() -> str: + return SERVER_NAME + + +def get_tool_name() -> str: + return TOOL_NAME + + +def get_input_fields() -> list[dict[str, Any]]: + return INPUT_FIELDS + + +def python_type_for_field(field: dict[str, Any]) -> type: + field_type = str(field.get("type", "string")).lower() + if field_type in {"integer", "int64", "int32"}: + return int + if field_type in {"number", "float", "double", "decimal"}: + return float + if field_type in {"boolean", "bool"}: + return bool + return str + + +def create_input_model() -> type[BaseModel]: + model_fields: dict[str, tuple[type, Field]] = {} + + for field in get_input_fields(): + field_name = field["name"] + description = field.get("description") or f"Input value for {field_name}" + model_fields[field_name] = ( + python_type_for_field(field), + Field(..., description=description), + ) + + return create_model("AutoAIInput", **model_fields) + + +def build_scoring_payload(input_model: BaseModel) -> dict[str, Any]: + fields = [field["name"] for field in get_input_fields()] + values = [getattr(input_model, field_name) for field_name in fields] + + return { + "input_data": [ + { + "fields": fields, + "values": [values], + } + ] + } + + +def extract_prediction(response: dict[str, Any]) -> Any: + try: + predictions = response["predictions"][0] + values = predictions["values"][0] + if len(values) == 1: + return values[0] + return values + except (KeyError, IndexError, TypeError) as error: + raise RuntimeError( + f"Unexpected response structure from deployment: {response}" + ) from error + + +def get_prediction_column() -> str: + return PREDICTION_COLUMN diff --git a/agents/community/mcp-orchestrate-autoai-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/generate_template.py new file mode 100644 index 0000000..5b1650e --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/generate_template.py @@ -0,0 +1,206 @@ +from pathlib import Path +from typing import Any + +import yaml +from dotenv import load_dotenv +from ibm_watsonx_ai import APIClient, Credentials + + +ROOT_DIR = Path(__file__).resolve().parent.parent +MCP_SERVER_DIR = ROOT_DIR / "mcp_server" +GENERATED_CONFIG_PATH = MCP_SERVER_DIR / "generated_config.py" +TOOLKIT_PATH = ROOT_DIR / "toolkit.yaml" +AGENT_PATH = ROOT_DIR / "agent.yaml" + +DEFAULT_TOOLKIT_NAME = "autoai-generic-toolkit" +DEFAULT_AGENT_NAME = "autoai_prediction_agent" +DEFAULT_TOOL_NAME = "get_autoai_prediction" +DEFAULT_SERVER_NAME = "autoai-generic-toolkit" +DEFAULT_LLM_NAME = "groq/openai/gpt-oss-120b" + + +def load_env() -> None: + load_dotenv(ROOT_DIR / ".env") + + +def prepare_api_client() -> APIClient: + return APIClient( + credentials=Credentials( + url=require_env("WATSONX_URL"), + api_key=require_env("WATSONX_API_KEY"), + ), + space_id=require_env("WATSONX_SPACE_ID"), + ) + + +def require_env(name: str) -> str: + import os + + value = os.getenv(name) + if not value: + raise ValueError(f"{name} is not set") + return value + + +def get_deployment_details(client: APIClient, deployment_id: str) -> dict[str, Any]: + return client.deployments.get_details(deployment_id) + + +def get_model_asset_id(deployment_details: dict[str, Any]) -> str: + entity = deployment_details.get("entity", {}) + asset = entity.get("asset", {}) + asset_id = asset.get("id") + if not asset_id: + raise RuntimeError("Could not determine model asset id from deployment details") + return asset_id + + +def get_model_asset_details(client: APIClient, asset_id: str) -> dict[str, Any]: + return client.data_assets.get_details(asset_id) + + +def get_input_fields(asset_details: dict[str, Any]) -> list[dict[str, Any]]: + schemas = asset_details["entity"]["wml_model"]["schemas"] + return schemas["input"][0]["fields"] + + +def get_label_column(asset_details: dict[str, Any]) -> str: + return asset_details["entity"]["wml_model"]["label_column"] + + +def build_metadata( + deployment_id: str, + input_fields: list[dict[str, Any]], + label_column: str, +) -> dict[str, Any]: + return { + "deployment_id": deployment_id, + "toolkit_name": DEFAULT_TOOLKIT_NAME, + "tool_name": DEFAULT_TOOL_NAME, + "agent_name": DEFAULT_AGENT_NAME, + "server_name": DEFAULT_SERVER_NAME, + "llm": DEFAULT_LLM_NAME, + "label_column": label_column, + "input_fields": input_fields, + } + + +def write_generated_config(metadata: dict[str, Any]) -> None: + content = ( + f'SERVER_NAME = "{metadata["server_name"]}"\n' + f'TOOL_NAME = "{metadata["tool_name"]}"\n' + f"TOOL_DESCRIPTION = \"Predict the value of '{metadata['label_column']}' using the deployed watsonx.ai AutoAI model. Provide all required input fields defined by the generated schema. Returns the prediction result from the deployment.\"\n" + f'PREDICTION_COLUMN = "{metadata["label_column"]}"\n' + f"INPUT_FIELDS = {repr(metadata['input_fields'])}\n" + ) + with open(GENERATED_CONFIG_PATH, "w", encoding="utf-8") as file: + file.write(content) + + +def build_toolkit_yaml() -> dict[str, Any]: + return { + "spec_version": "v1", + "kind": "mcp", + "name": DEFAULT_TOOLKIT_NAME, + "description": "Generic watsonx.ai AutoAI prediction toolkit", + "command": "python server.py", + "env": [ + "WATSONX_URL", + "WATSONX_API_KEY", + "WATSONX_SPACE_ID", + "WATSONX_AUTOAI_DEPLOYMENT_ID", + ], + "tools": ["*"], + "package_root": "./mcp_server", + } + + +def build_agent_yaml( + label_column: str, + input_fields: list[dict[str, Any]], +) -> dict[str, Any]: + field_names = [field["name"] for field in input_fields] + field_list = "\n".join([f" - {name}" for name in field_names]) + + instructions = ( + f"You are a prediction assistant for the '{label_column}' column.\n\n" + "STRICT RULES — follow these without exception:\n" + "1. NEVER answer prediction questions from your own knowledge or reasoning.\n" + f"2. ALWAYS call {DEFAULT_TOOL_NAME} when the user provides all required input fields.\n" + "3. If one or more required fields are missing, ask only for the missing fields.\n" + "4. After the tool returns a result, report the prediction clearly.\n" + "5. Do NOT perform your own calculations.\n\n" + "Required input fields:\n" + f"{field_list}" + ) + + return { + "spec_version": "v1", + "kind": "native", + "name": DEFAULT_AGENT_NAME, + "description": ( + f"Predicts the '{label_column}' column using a watsonx.ai deployed AutoAI model." + ), + "llm": DEFAULT_LLM_NAME, + "style": "react", + "hide_reasoning": False, + "instructions": instructions, + "tools": [f"{DEFAULT_TOOLKIT_NAME}:{DEFAULT_TOOL_NAME}"], + "collaborators": [], + } + + +class IndentedListDumper(yaml.SafeDumper): + def increase_indent(self, flow: bool = False, indentless: bool = False): + return super().increase_indent(flow=flow, indentless=False) + + +def write_yaml(path: Path, content: dict[str, Any]) -> None: + with open(path, "w", encoding="utf-8") as file: + yaml.dump( + content, + file, + Dumper=IndentedListDumper, + sort_keys=False, + allow_unicode=True, + default_flow_style=False, + indent=2, + ) + + +def main() -> None: + load_env() + client = prepare_api_client() + + deployment_id = require_env("WATSONX_AUTOAI_DEPLOYMENT_ID") + + deployment_details = get_deployment_details(client, deployment_id) + asset_id = get_model_asset_id(deployment_details) + asset_details = get_model_asset_details(client, asset_id) + + input_fields = get_input_fields(asset_details) + label_column = get_label_column(asset_details) + + metadata = build_metadata( + deployment_id=deployment_id, + input_fields=input_fields, + label_column=label_column, + ) + write_generated_config(metadata) + + toolkit_yaml = build_toolkit_yaml() + agent_yaml = build_agent_yaml( + label_column=label_column, + input_fields=input_fields, + ) + + write_yaml(TOOLKIT_PATH, toolkit_yaml) + write_yaml(AGENT_PATH, agent_yaml) + + print(f"Generated {TOOLKIT_PATH}") + print(f"Generated {AGENT_PATH}") + print(f"Generated {GENERATED_CONFIG_PATH}") + + +if __name__ == "__main__": + main() diff --git a/agents/community/mcp-orchestrate-autoai-template-generic/template.env b/agents/community/mcp-orchestrate-autoai-template-generic/template.env new file mode 100644 index 0000000..c6ea99b --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-template-generic/template.env @@ -0,0 +1,5 @@ +WATSONX_URL=https://us-south.ml.cloud.ibm.com +WATSONX_API_KEY=your_api_key +WATSONX_SPACE_ID=your_space_id + +WATSONX_AUTOAI_DEPLOYMENT_ID=your_autoai_deployment_id \ No newline at end of file From 73221f39e3669290b3a1c5c288d298a70c3e2483 Mon Sep 17 00:00:00 2001 From: Mateusz Szewczyk Date: Tue, 2 Jun 2026 14:40:54 +0200 Subject: [PATCH 02/12] remove tool description from config --- .../scripts/generate_template.py | 1 - 1 file changed, 1 deletion(-) diff --git a/agents/community/mcp-orchestrate-autoai-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/generate_template.py index 5b1650e..e429a0d 100644 --- a/agents/community/mcp-orchestrate-autoai-template-generic/scripts/generate_template.py +++ b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/generate_template.py @@ -89,7 +89,6 @@ def write_generated_config(metadata: dict[str, Any]) -> None: content = ( f'SERVER_NAME = "{metadata["server_name"]}"\n' f'TOOL_NAME = "{metadata["tool_name"]}"\n' - f"TOOL_DESCRIPTION = \"Predict the value of '{metadata['label_column']}' using the deployed watsonx.ai AutoAI model. Provide all required input fields defined by the generated schema. Returns the prediction result from the deployment.\"\n" f'PREDICTION_COLUMN = "{metadata["label_column"]}"\n' f"INPUT_FIELDS = {repr(metadata['input_fields'])}\n" ) From 44accc924b895f6fa1fe6c10dcbfe36ec3aa4e6f Mon Sep 17 00:00:00 2001 From: Mateusz Szewczyk Date: Wed, 3 Jun 2026 11:48:26 +0200 Subject: [PATCH 03/12] added script for automatic deploy and cleanup --- .../scripts/cleanup.sh | 51 ++++++++ .../scripts/deploy.sh | 121 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100755 agents/community/mcp-orchestrate-autoai-template-generic/scripts/cleanup.sh create mode 100755 agents/community/mcp-orchestrate-autoai-template-generic/scripts/deploy.sh diff --git a/agents/community/mcp-orchestrate-autoai-template-generic/scripts/cleanup.sh b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/cleanup.sh new file mode 100755 index 0000000..bc8c0ed --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/cleanup.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Cleanup script for AutoAI orchestration resources +# This script removes all created items with individual error handling +# Each deletion is wrapped in a try-catch equivalent to handle non-existent resources + +echo "Starting cleanup of AutoAI orchestration resources..." +echo "==================================================" + +# Step 1: Undeploy the agent +echo "" +echo "Step 1: Undeploying agent 'autoai_prediction_agent'..." +if orchestrate agents undeploy --name autoai_prediction_agent 2>/dev/null; then + echo "✓ Agent undeployed successfully" +else + echo "⚠ Failed to undeploy agent (may not exist or already undeployed)" +fi + +# Step 2: Remove the agent +echo "" +echo "Step 2: Removing agent 'autoai_prediction_agent'..." +if orchestrate agents remove --name autoai_prediction_agent --kind native 2>/dev/null; then + echo "✓ Agent removed successfully" +else + echo "⚠ Failed to remove agent (may not exist)" +fi + +# Step 3: Remove the toolkit +echo "" +echo "Step 3: Removing toolkit 'autoai-generic-toolkit'..." +if orchestrate toolkits remove --name autoai-generic-toolkit 2>/dev/null; then + echo "✓ Toolkit removed successfully" +else + echo "⚠ Failed to remove toolkit (may not exist)" +fi + +# Step 4: Remove the connection +echo "" +echo "Step 4: Removing connection 'autoai-prediction-connection'..." +if orchestrate connections remove --app-id autoai-prediction-connection 2>/dev/null; then + echo "✓ Connection removed successfully" +else + echo "⚠ Failed to remove connection (may not exist)" +fi + +echo "" +echo "==================================================" +echo "Cleanup process completed!" +echo "Note: Warnings indicate resources that were already removed or never existed." + +# Made with Bob diff --git a/agents/community/mcp-orchestrate-autoai-template-generic/scripts/deploy.sh b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/deploy.sh new file mode 100755 index 0000000..363d9a2 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/deploy.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# Deployment script for AutoAI orchestration resources +# This script creates and configures all required resources with individual error handling +# Each step is wrapped in a try-catch equivalent to handle errors gracefully + +echo "Starting deployment of AutoAI orchestration resources..." +echo "=========================================================" + +# Check if .env file exists +if [ ! -f .env ]; then + echo "❌ Error: .env file not found in current directory" + echo "Please create a .env file with required environment variables" + exit 1 +fi + +# Step 1: Add connection +echo "" +echo "Step 1: Adding connection 'autoai-prediction-connection'..." +if orchestrate connections add -a autoai-prediction-connection 2>/dev/null; then + echo "✓ Connection added successfully" +else + echo "⚠ Failed to add connection (may already exist)" +fi + +# Step 2: Export environment variables from .env file +echo "" +echo "Step 2: Loading environment variables from .env file..." +if export $(grep -v '^#' .env | xargs) 2>/dev/null; then + echo "✓ Environment variables loaded successfully" + echo " - WATSONX_URL: ${WATSONX_URL:0:30}..." + echo " - WATSONX_API_KEY: ${WATSONX_API_KEY:0:10}..." + echo " - WATSONX_SPACE_ID: $WATSONX_SPACE_ID" + echo " - WATSONX_AUTOAI_DEPLOYMENT_ID: $WATSONX_AUTOAI_DEPLOYMENT_ID" +else + echo "❌ Failed to load environment variables from .env file" + exit 1 +fi + +# Step 3: Configure connections for both draft and live environments +echo "" +echo "Step 3: Configuring connections for draft and live environments..." + +for env in draft live; do + echo "" + echo " Configuring $env environment..." + + # Configure connection + if orchestrate connections configure \ + -a autoai-prediction-connection \ + --env $env \ + --type team \ + --kind key_value 2>/dev/null; then + echo " ✓ Connection configured for $env environment" + else + echo " ⚠ Failed to configure connection for $env environment" + continue + fi + + # Set credentials + if orchestrate connections set-credentials \ + -a autoai-prediction-connection \ + --env $env \ + -e "WATSONX_URL=$WATSONX_URL" \ + -e "WATSONX_API_KEY=$WATSONX_API_KEY" \ + -e "WATSONX_SPACE_ID=$WATSONX_SPACE_ID" \ + -e "WATSONX_AUTOAI_DEPLOYMENT_ID=$WATSONX_AUTOAI_DEPLOYMENT_ID" 2>/dev/null; then + echo " ✓ Credentials set for $env environment" + else + echo " ⚠ Failed to set credentials for $env environment" + fi +done + +# Step 4: Import toolkit +echo "" +echo "Step 4: Importing toolkit from toolkit.yaml..." +if [ ! -f toolkit.yaml ]; then + echo "❌ Error: toolkit.yaml file not found in current directory" + exit 1 +fi + +if orchestrate toolkits import -f toolkit.yaml -a autoai-prediction-connection 2>/dev/null; then + echo "✓ Toolkit imported successfully" +else + echo "⚠ Failed to import toolkit (may already exist or invalid configuration)" +fi + +# Step 5: Import agent +echo "" +echo "Step 5: Importing agent from agent.yaml..." +if [ ! -f agent.yaml ]; then + echo "❌ Error: agent.yaml file not found in current directory" + exit 1 +fi + +if orchestrate agents import -f agent.yaml 2>/dev/null; then + echo "✓ Agent imported successfully" +else + echo "⚠ Failed to import agent (may already exist or invalid configuration)" +fi + +# Step 6: Deploy agent +echo "" +echo "Step 6: Deploying agent 'autoai_prediction_agent'..." +if orchestrate agents deploy --name autoai_prediction_agent 2>/dev/null; then + echo "✓ Agent deployed successfully" +else + echo "⚠ Failed to deploy agent (may already be deployed or configuration error)" +fi + +echo "" +echo "=========================================================" +echo "Deployment process completed!" +echo "Note: Warnings indicate resources that already exist or configuration issues." +echo "" +echo "To verify the deployment, you can run:" +echo " orchestrate agents list" +echo " orchestrate toolkits list" +echo " orchestrate connections list" + +# Made with Bob \ No newline at end of file From 5feee05972855cf9b30ca24a26baf7dff74dfbd7 Mon Sep 17 00:00:00 2001 From: Mateusz Szewczyk Date: Wed, 3 Jun 2026 12:11:58 +0200 Subject: [PATCH 04/12] update deploy script --- .../scripts/deploy.sh | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/agents/community/mcp-orchestrate-autoai-template-generic/scripts/deploy.sh b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/deploy.sh index 363d9a2..c3a4126 100755 --- a/agents/community/mcp-orchestrate-autoai-template-generic/scripts/deploy.sh +++ b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/deploy.sh @@ -14,18 +14,36 @@ if [ ! -f .env ]; then exit 1 fi -# Step 1: Add connection +# Step 1: Generate template files (toolkit.yaml, agent.yaml, generated_config.py) echo "" -echo "Step 1: Adding connection 'autoai-prediction-connection'..." +echo "Step 1: Generating template files from AutoAI deployment..." +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if python "$SCRIPT_DIR/generate_template.py" 2>/dev/null; then + echo "✓ Template files generated successfully" + echo " - toolkit.yaml" + echo " - agent.yaml" + echo " - mcp_server/generated_config.py" +else + echo "❌ Failed to generate template files" + echo "Please ensure:" + echo " - Python dependencies are installed (ibm-watsonx-ai, python-dotenv, pyyaml)" + echo " - .env file contains valid credentials" + echo " - WATSONX_AUTOAI_DEPLOYMENT_ID is correct" + exit 1 +fi + +# Step 2: Add connection +echo "" +echo "Step 2: Adding connection 'autoai-prediction-connection'..." if orchestrate connections add -a autoai-prediction-connection 2>/dev/null; then echo "✓ Connection added successfully" else echo "⚠ Failed to add connection (may already exist)" fi -# Step 2: Export environment variables from .env file +# Step 3: Export environment variables from .env file echo "" -echo "Step 2: Loading environment variables from .env file..." +echo "Step 3: Loading environment variables from .env file..." if export $(grep -v '^#' .env | xargs) 2>/dev/null; then echo "✓ Environment variables loaded successfully" echo " - WATSONX_URL: ${WATSONX_URL:0:30}..." @@ -37,9 +55,9 @@ else exit 1 fi -# Step 3: Configure connections for both draft and live environments +# Step 4: Configure connections for both draft and live environments echo "" -echo "Step 3: Configuring connections for draft and live environments..." +echo "Step 4: Configuring connections for draft and live environments..." for env in draft live; do echo "" @@ -71,9 +89,9 @@ for env in draft live; do fi done -# Step 4: Import toolkit +# Step 5: Import toolkit echo "" -echo "Step 4: Importing toolkit from toolkit.yaml..." +echo "Step 5: Importing toolkit from toolkit.yaml..." if [ ! -f toolkit.yaml ]; then echo "❌ Error: toolkit.yaml file not found in current directory" exit 1 @@ -85,9 +103,9 @@ else echo "⚠ Failed to import toolkit (may already exist or invalid configuration)" fi -# Step 5: Import agent +# Step 6: Import agent echo "" -echo "Step 5: Importing agent from agent.yaml..." +echo "Step 6: Importing agent from agent.yaml..." if [ ! -f agent.yaml ]; then echo "❌ Error: agent.yaml file not found in current directory" exit 1 @@ -99,9 +117,9 @@ else echo "⚠ Failed to import agent (may already exist or invalid configuration)" fi -# Step 6: Deploy agent +# Step 7: Deploy agent echo "" -echo "Step 6: Deploying agent 'autoai_prediction_agent'..." +echo "Step 7: Deploying agent 'autoai_prediction_agent'..." if orchestrate agents deploy --name autoai_prediction_agent 2>/dev/null; then echo "✓ Agent deployed successfully" else From 861474b4adbba6596751afd1db053bd59e7a2442 Mon Sep 17 00:00:00 2001 From: Karol Zmorski Date: Fri, 12 Jun 2026 15:43:22 +0200 Subject: [PATCH 05/12] Init commit --- .../mcp_server/requirements.txt | 5 + .../mcp_server/server.py | 54 ++++++ .../mcp_server/utils.py | 44 +++++ .../scripts/cleanup.sh | 51 +++++ .../scripts/deploy.sh | 139 ++++++++++++++ .../scripts/generate_template.py | 177 ++++++++++++++++++ .../template.env | 5 + 7 files changed, 475 insertions(+) create mode 100644 agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/requirements.txt create mode 100644 agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py create mode 100644 agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py create mode 100644 agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh create mode 100644 agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh create mode 100644 agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py create mode 100644 agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/template.env diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/requirements.txt b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/requirements.txt new file mode 100644 index 0000000..b626e2e --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/requirements.txt @@ -0,0 +1,5 @@ +mcp +python-dotenv +ibm-watsonx-ai +pydantic +pyyaml \ No newline at end of file diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py new file mode 100644 index 0000000..a9bdd94 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py @@ -0,0 +1,54 @@ +from mcp.server.fastmcp import FastMCP + +from utils import ( + get_deployment_name, + get_rag_deployment_id, + get_server_name, + get_tool_name, + prepare_api_client, +) + +mcp = FastMCP(get_server_name()) + + +def _build_tool_description() -> str: + deployment_name = get_deployment_name() + + return ( + f"Ask a question to the '{deployment_name}' RAG Pattern AI service.\n\n" + "Provide a question as a string, and the AI service will return an answer " + "based on its knowledge base.\n\n" + "Returns the answer from the RAG Pattern deployment." + ) + + +@mcp.tool(name=get_tool_name(), description=_build_tool_description()) +def ask_rag_question(question: str): + """ + Ask a question to the AutoAI RAG Pattern AI service. + + Args: + question: The question to ask the RAG Pattern AI service + + Returns: + dict: Contains the answer from the AI service + """ + api_client = prepare_api_client() + deployment_id = get_rag_deployment_id() + + payload = {"messages": [{"role": "user", "content": question}]} + + score_response = api_client.deployments.run_ai_service(deployment_id, payload) + + output_for_user = score_response["choices"][0]["message"]["content"] + + return { + "question": question, + "answer": output_for_user, + } + + +if __name__ == "__main__": + mcp.run(transport="stdio") + +# Made with Bob diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py new file mode 100644 index 0000000..de887d0 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py @@ -0,0 +1,44 @@ +import os + +from dotenv import load_dotenv +from ibm_watsonx_ai import APIClient, Credentials + +from generated_config import DEPLOYMENT_NAME, SERVER_NAME, TOOL_NAME + + +def load_env() -> None: + load_dotenv() + + +def prepare_api_client() -> APIClient: + load_env() + return APIClient( + credentials=Credentials( + url=os.getenv("WATSONX_URL"), + api_key=os.getenv("WATSONX_API_KEY"), + ), + space_id=os.getenv("WATSONX_SPACE_ID"), + ) + + +def get_rag_deployment_id() -> str: + load_env() + deployment_id = os.getenv("WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID") + if not deployment_id: + raise ValueError("WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID is not set") + return deployment_id + + +def get_server_name() -> str: + return SERVER_NAME + + +def get_tool_name() -> str: + return TOOL_NAME + + +def get_deployment_name() -> str: + return DEPLOYMENT_NAME + + +# Made with Bob diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh new file mode 100644 index 0000000..832b0f0 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Cleanup script for AutoAI RAG Pattern orchestration resources +# This script removes all created items with individual error handling +# Each deletion is wrapped in a try-catch equivalent to handle non-existent resources + +echo "Starting cleanup of AutoAI RAG Pattern orchestration resources..." +echo "==================================================" + +# Step 1: Undeploy the agent +echo "" +echo "Step 1: Undeploying agent 'autoai_rag_pattern_agent'..." +if orchestrate agents undeploy --name autoai_rag_pattern_agent 2>/dev/null; then + echo "✓ Agent undeployed successfully" +else + echo "⚠ Failed to undeploy agent (may not exist or already undeployed)" +fi + +# Step 2: Remove the agent +echo "" +echo "Step 2: Removing agent 'autoai_rag_pattern_agent'..." +if orchestrate agents remove --name autoai_rag_pattern_agent --kind native 2>/dev/null; then + echo "✓ Agent removed successfully" +else + echo "⚠ Failed to remove agent (may not exist)" +fi + +# Step 3: Remove the toolkit +echo "" +echo "Step 3: Removing toolkit 'autoai-rag-pattern-toolkit'..." +if orchestrate toolkits remove --name autoai-rag-pattern-toolkit 2>/dev/null; then + echo "✓ Toolkit removed successfully" +else + echo "⚠ Failed to remove toolkit (may not exist)" +fi + +# Step 4: Remove the connection +echo "" +echo "Step 4: Removing connection 'autoai-rag-pattern-connection'..." +if orchestrate connections remove --app-id autoai-rag-pattern-connection 2>/dev/null; then + echo "✓ Connection removed successfully" +else + echo "⚠ Failed to remove connection (may not exist)" +fi + +echo "" +echo "==================================================" +echo "Cleanup process completed!" +echo "Note: Warnings indicate resources that were already removed or never existed." + +# Made with Bob \ No newline at end of file diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh new file mode 100644 index 0000000..7713a08 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +# Deployment script for AutoAI RAG Pattern orchestration resources +# This script creates and configures all required resources with individual error handling +# Each step is wrapped in a try-catch equivalent to handle errors gracefully + +echo "Starting deployment of AutoAI RAG Pattern orchestration resources..." +echo "=========================================================" + +# Check if .env file exists +if [ ! -f .env ]; then + echo "❌ Error: .env file not found in current directory" + echo "Please create a .env file with required environment variables" + exit 1 +fi + +# Step 1: Generate template files (toolkit.yaml, agent.yaml, generated_config.py) +echo "" +echo "Step 1: Generating template files from AutoAI RAG Pattern deployment..." +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if python "$SCRIPT_DIR/generate_template.py" 2>/dev/null; then + echo "✓ Template files generated successfully" + echo " - toolkit.yaml" + echo " - agent.yaml" + echo " - mcp_server/generated_config.py" +else + echo "❌ Failed to generate template files" + echo "Please ensure:" + echo " - Python dependencies are installed (ibm-watsonx-ai, python-dotenv, pyyaml)" + echo " - .env file contains valid credentials" + echo " - WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID is correct" + exit 1 +fi + +# Step 2: Add connection +echo "" +echo "Step 2: Adding connection 'autoai-rag-pattern-connection'..." +if orchestrate connections add -a autoai-rag-pattern-connection 2>/dev/null; then + echo "✓ Connection added successfully" +else + echo "⚠ Failed to add connection (may already exist)" +fi + +# Step 3: Export environment variables from .env file +echo "" +echo "Step 3: Loading environment variables from .env file..." +if export $(grep -v '^#' .env | xargs) 2>/dev/null; then + echo "✓ Environment variables loaded successfully" + echo " - WATSONX_URL: ${WATSONX_URL:0:30}..." + echo " - WATSONX_API_KEY: ${WATSONX_API_KEY:0:10}..." + echo " - WATSONX_SPACE_ID: $WATSONX_SPACE_ID" + echo " - WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID: $WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID" +else + echo "❌ Failed to load environment variables from .env file" + exit 1 +fi + +# Step 4: Configure connections for both draft and live environments +echo "" +echo "Step 4: Configuring connections for draft and live environments..." + +for env in draft live; do + echo "" + echo " Configuring $env environment..." + + # Configure connection + if orchestrate connections configure \ + -a autoai-rag-pattern-connection \ + --env $env \ + --type team \ + --kind key_value 2>/dev/null; then + echo " ✓ Connection configured for $env environment" + else + echo " ⚠ Failed to configure connection for $env environment" + continue + fi + + # Set credentials + if orchestrate connections set-credentials \ + -a autoai-rag-pattern-connection \ + --env $env \ + -e "WATSONX_URL=$WATSONX_URL" \ + -e "WATSONX_API_KEY=$WATSONX_API_KEY" \ + -e "WATSONX_SPACE_ID=$WATSONX_SPACE_ID" \ + -e "WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID=$WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID" 2>/dev/null; then + echo " ✓ Credentials set for $env environment" + else + echo " ⚠ Failed to set credentials for $env environment" + fi +done + +# Step 5: Import toolkit +echo "" +echo "Step 5: Importing toolkit from toolkit.yaml..." +if [ ! -f toolkit.yaml ]; then + echo "❌ Error: toolkit.yaml file not found in current directory" + exit 1 +fi + +if orchestrate toolkits import -f toolkit.yaml -a autoai-rag-pattern-connection 2>/dev/null; then + echo "✓ Toolkit imported successfully" +else + echo "⚠ Failed to import toolkit (may already exist or invalid configuration)" +fi + +# Step 6: Import agent +echo "" +echo "Step 6: Importing agent from agent.yaml..." +if [ ! -f agent.yaml ]; then + echo "❌ Error: agent.yaml file not found in current directory" + exit 1 +fi + +if orchestrate agents import -f agent.yaml 2>/dev/null; then + echo "✓ Agent imported successfully" +else + echo "⚠ Failed to import agent (may already exist or invalid configuration)" +fi + +# Step 7: Deploy agent +echo "" +echo "Step 7: Deploying agent 'autoai_rag_pattern_agent'..." +if orchestrate agents deploy --name autoai_rag_pattern_agent 2>/dev/null; then + echo "✓ Agent deployed successfully" +else + echo "⚠ Failed to deploy agent (may already be deployed or configuration error)" +fi + +echo "" +echo "=========================================================" +echo "Deployment process completed!" +echo "Note: Warnings indicate resources that already exist or configuration issues." +echo "" +echo "To verify the deployment, you can run:" +echo " orchestrate agents list" +echo " orchestrate toolkits list" +echo " orchestrate connections list" + +# Made with Bob \ No newline at end of file diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py new file mode 100644 index 0000000..3be1656 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py @@ -0,0 +1,177 @@ +from pathlib import Path +from typing import Any + +import yaml +from dotenv import load_dotenv +from ibm_watsonx_ai import APIClient, Credentials + + +ROOT_DIR = Path(__file__).resolve().parent.parent +MCP_SERVER_DIR = ROOT_DIR / "mcp_server" +GENERATED_CONFIG_PATH = MCP_SERVER_DIR / "generated_config.py" +TOOLKIT_PATH = ROOT_DIR / "toolkit.yaml" +AGENT_PATH = ROOT_DIR / "agent.yaml" + +DEFAULT_TOOLKIT_NAME = "autoai-rag-pattern-toolkit" +DEFAULT_AGENT_NAME = "autoai_rag_pattern_agent" +DEFAULT_TOOL_NAME = "ask_rag_question" +DEFAULT_SERVER_NAME = "autoai-rag-pattern-toolkit" +DEFAULT_LLM_NAME = "groq/openai/gpt-oss-120b" + + +def load_env() -> None: + load_dotenv(ROOT_DIR / ".env") + + +def prepare_api_client() -> APIClient: + return APIClient( + credentials=Credentials( + url=require_env("WATSONX_URL"), + api_key=require_env("WATSONX_API_KEY"), + ), + space_id=require_env("WATSONX_SPACE_ID"), + ) + + +def require_env(name: str) -> str: + import os + + value = os.getenv(name) + if not value: + raise ValueError(f"{name} is not set") + return value + + +def get_deployment_details(client: APIClient, deployment_id: str) -> dict[str, Any]: + return client.deployments.get_details(deployment_id) + + +def get_deployment_name(deployment_details: dict[str, Any]) -> str: + metadata = deployment_details.get("metadata", {}) + name = metadata.get("name") + if not name: + raise RuntimeError( + "Could not determine deployment name from deployment details" + ) + return name + + +def build_metadata( + deployment_id: str, + deployment_name: str, +) -> dict[str, Any]: + return { + "deployment_id": deployment_id, + "deployment_name": deployment_name, + "toolkit_name": DEFAULT_TOOLKIT_NAME, + "tool_name": DEFAULT_TOOL_NAME, + "agent_name": DEFAULT_AGENT_NAME, + "server_name": DEFAULT_SERVER_NAME, + "llm": DEFAULT_LLM_NAME, + } + + +def write_generated_config(metadata: dict[str, Any]) -> None: + content = ( + f'SERVER_NAME = "{metadata["server_name"]}"\n' + f'TOOL_NAME = "{metadata["tool_name"]}"\n' + f'DEPLOYMENT_NAME = "{metadata["deployment_name"]}"\n' + ) + with open(GENERATED_CONFIG_PATH, "w", encoding="utf-8") as file: + file.write(content) + + +def build_toolkit_yaml() -> dict[str, Any]: + return { + "spec_version": "v1", + "kind": "mcp", + "name": DEFAULT_TOOLKIT_NAME, + "description": "Generic watsonx.ai AutoAI RAG Pattern toolkit", + "command": "python server.py", + "env": [ + "WATSONX_URL", + "WATSONX_API_KEY", + "WATSONX_SPACE_ID", + "WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID", + ], + "tools": ["*"], + "package_root": "./mcp_server", + } + + +def build_agent_yaml(deployment_name: str) -> dict[str, Any]: + instructions = ( + f"You are a question-answering assistant powered by the '{deployment_name}' AutoAI RAG Pattern AI service.\n\n" + "STRICT RULES — follow these without exception:\n" + "1. NEVER answer questions from your own knowledge or reasoning.\n" + f"2. ALWAYS call {DEFAULT_TOOL_NAME} when the user asks a question.\n" + "3. After the tool returns a result, present the answer clearly to the user.\n" + "4. Do NOT make up answers or use your own knowledge.\n" + "5. If the tool returns an error, inform the user and ask them to try again.\n\n" + "Your role is to pass user questions to the RAG Pattern AI service and relay the responses." + ) + + return { + "spec_version": "v1", + "kind": "native", + "name": DEFAULT_AGENT_NAME, + "description": ( + f"Answers questions using the '{deployment_name}' watsonx.ai deployed AutoAI RAG Pattern AI service." + ), + "llm": DEFAULT_LLM_NAME, + "style": "react", + "hide_reasoning": False, + "instructions": instructions, + "tools": [f"{DEFAULT_TOOLKIT_NAME}:{DEFAULT_TOOL_NAME}"], + "collaborators": [], + } + + +class IndentedListDumper(yaml.SafeDumper): + def increase_indent(self, flow: bool = False, indentless: bool = False): + return super().increase_indent(flow=flow, indentless=False) + + +def write_yaml(path: Path, content: dict[str, Any]) -> None: + with open(path, "w", encoding="utf-8") as file: + yaml.dump( + content, + file, + Dumper=IndentedListDumper, + sort_keys=False, + allow_unicode=True, + default_flow_style=False, + indent=2, + ) + + +def main() -> None: + load_env() + client = prepare_api_client() + + deployment_id = require_env("WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID") + + deployment_details = get_deployment_details(client, deployment_id) + deployment_name = get_deployment_name(deployment_details) + + metadata = build_metadata( + deployment_id=deployment_id, + deployment_name=deployment_name, + ) + write_generated_config(metadata) + + toolkit_yaml = build_toolkit_yaml() + agent_yaml = build_agent_yaml(deployment_name=deployment_name) + + write_yaml(TOOLKIT_PATH, toolkit_yaml) + write_yaml(AGENT_PATH, agent_yaml) + + print(f"Generated {TOOLKIT_PATH}") + print(f"Generated {AGENT_PATH}") + print(f"Generated {GENERATED_CONFIG_PATH}") + + +if __name__ == "__main__": + main() + +# Made with Bob diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/template.env b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/template.env new file mode 100644 index 0000000..eeb809e --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/template.env @@ -0,0 +1,5 @@ +WATSONX_URL=https://us-south.ml.cloud.ibm.com +WATSONX_API_KEY=your_api_key +WATSONX_SPACE_ID=your_space_id + +WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID=your_autoai_rag_pattern_deployment_id \ No newline at end of file From 87dbe19380cfc57b309d7e2dfaa5a1e9ce737b85 Mon Sep 17 00:00:00 2001 From: Karol Zmorski Date: Fri, 12 Jun 2026 16:36:33 +0200 Subject: [PATCH 06/12] Improve implementation --- .../mcp_server/server.py | 24 ++++++++----------- .../scripts/generate_template.py | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py index a9bdd94..e6580a2 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py @@ -1,4 +1,5 @@ from mcp.server.fastmcp import FastMCP +from pydantic import BaseModel, Field from utils import ( get_deployment_name, @@ -11,6 +12,12 @@ mcp = FastMCP(get_server_name()) +class RAGQuestionInput(BaseModel): + question: str = Field( + ..., description="The question to ask the RAG Pattern AI service" + ) + + def _build_tool_description() -> str: deployment_name = get_deployment_name() @@ -23,32 +30,21 @@ def _build_tool_description() -> str: @mcp.tool(name=get_tool_name(), description=_build_tool_description()) -def ask_rag_question(question: str): - """ - Ask a question to the AutoAI RAG Pattern AI service. - - Args: - question: The question to ask the RAG Pattern AI service - - Returns: - dict: Contains the answer from the AI service - """ +def invoke_rag_question(input_data: RAGQuestionInput): api_client = prepare_api_client() deployment_id = get_rag_deployment_id() - payload = {"messages": [{"role": "user", "content": question}]} + payload = {"messages": [{"role": "user", "content": input_data.question}]} score_response = api_client.deployments.run_ai_service(deployment_id, payload) output_for_user = score_response["choices"][0]["message"]["content"] return { - "question": question, + "question": input_data.question, "answer": output_for_user, } if __name__ == "__main__": mcp.run(transport="stdio") - -# Made with Bob diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py index 3be1656..114c063 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py @@ -14,7 +14,7 @@ DEFAULT_TOOLKIT_NAME = "autoai-rag-pattern-toolkit" DEFAULT_AGENT_NAME = "autoai_rag_pattern_agent" -DEFAULT_TOOL_NAME = "ask_rag_question" +DEFAULT_TOOL_NAME = "get_rag_pattern_answer" DEFAULT_SERVER_NAME = "autoai-rag-pattern-toolkit" DEFAULT_LLM_NAME = "groq/openai/gpt-oss-120b" From b97ca44da1a3e788fe9d7d829ca9148cc3bccde8 Mon Sep 17 00:00:00 2001 From: Karol Zmorski Date: Thu, 18 Jun 2026 12:10:55 +0200 Subject: [PATCH 07/12] Orchestrate agent V2 - more generic, optional tool calling --- .../mcp_server/server.py | 23 +++++++++- .../mcp_server/utils.py | 26 +++++++++-- .../scripts/cleanup.sh | 14 +++--- .../scripts/deploy.sh | 6 +-- .../scripts/generate_template.py | 44 +++++++++++-------- 5 files changed, 77 insertions(+), 36 deletions(-) diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py index e6580a2..a46499e 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py @@ -3,9 +3,11 @@ from utils import ( get_deployment_name, + get_rag_answer_tool_name, get_rag_deployment_id, + get_rag_deployment_details_tool_name, + get_required_env_status, get_server_name, - get_tool_name, prepare_api_client, ) @@ -29,7 +31,7 @@ def _build_tool_description() -> str: ) -@mcp.tool(name=get_tool_name(), description=_build_tool_description()) +@mcp.tool(name=get_rag_answer_tool_name(), description=_build_tool_description()) def invoke_rag_question(input_data: RAGQuestionInput): api_client = prepare_api_client() deployment_id = get_rag_deployment_id() @@ -43,6 +45,23 @@ def invoke_rag_question(input_data: RAGQuestionInput): return { "question": input_data.question, "answer": output_for_user, + "deployment_name": get_deployment_name(), + "deployment_id": deployment_id, + } + + +@mcp.tool( + name=get_rag_deployment_details_tool_name(), + description="Return configuration-oriented details about the watsonx.ai AutoAI RAG deployment used by this toolkit.", +) +def get_rag_pattern_deployment_details(): + deployment_id = get_rag_deployment_id() + + return { + "deployment_name": get_deployment_name(), + "deployment_id": deployment_id, + "server_name": get_server_name(), + "required_environment_variables": get_required_env_status(), } diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py index de887d0..f82324a 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py @@ -3,7 +3,12 @@ from dotenv import load_dotenv from ibm_watsonx_ai import APIClient, Credentials -from generated_config import DEPLOYMENT_NAME, SERVER_NAME, TOOL_NAME +from generated_config import ( + DEPLOYMENT_NAME, + RAG_ANSWER_TOOL_NAME, + RAG_DEPLOYMENT_DETAILS_TOOL_NAME, + SERVER_NAME, +) def load_env() -> None: @@ -33,12 +38,25 @@ def get_server_name() -> str: return SERVER_NAME -def get_tool_name() -> str: - return TOOL_NAME +def get_rag_answer_tool_name() -> str: + return RAG_ANSWER_TOOL_NAME + + +def get_rag_deployment_details_tool_name() -> str: + return RAG_DEPLOYMENT_DETAILS_TOOL_NAME def get_deployment_name() -> str: return DEPLOYMENT_NAME -# Made with Bob +def get_required_env_status() -> dict: + load_env() + required_env_vars = [ + "WATSONX_URL", + "WATSONX_API_KEY", + "WATSONX_SPACE_ID", + "WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID", + ] + + return {env_var: bool(os.getenv(env_var)) for env_var in required_env_vars} diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh index 832b0f0..1cdb480 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh @@ -9,8 +9,8 @@ echo "==================================================" # Step 1: Undeploy the agent echo "" -echo "Step 1: Undeploying agent 'autoai_rag_pattern_agent'..." -if orchestrate agents undeploy --name autoai_rag_pattern_agent 2>/dev/null; then +echo "Step 1: Undeploying agent 'autoai_rag_pattern_agent_v2'..." +if orchestrate agents undeploy --name autoai_rag_pattern_agent_v2 2>/dev/null; then echo "✓ Agent undeployed successfully" else echo "⚠ Failed to undeploy agent (may not exist or already undeployed)" @@ -18,8 +18,8 @@ fi # Step 2: Remove the agent echo "" -echo "Step 2: Removing agent 'autoai_rag_pattern_agent'..." -if orchestrate agents remove --name autoai_rag_pattern_agent --kind native 2>/dev/null; then +echo "Step 2: Removing agent 'autoai_rag_pattern_agent_v2'..." +if orchestrate agents remove --name autoai_rag_pattern_agent_v2 --kind native 2>/dev/null; then echo "✓ Agent removed successfully" else echo "⚠ Failed to remove agent (may not exist)" @@ -27,8 +27,8 @@ fi # Step 3: Remove the toolkit echo "" -echo "Step 3: Removing toolkit 'autoai-rag-pattern-toolkit'..." -if orchestrate toolkits remove --name autoai-rag-pattern-toolkit 2>/dev/null; then +echo "Step 3: Removing toolkit 'autoai-rag-pattern-toolkit-v2'..." +if orchestrate toolkits remove --name autoai-rag-pattern-toolkit-v2 2>/dev/null; then echo "✓ Toolkit removed successfully" else echo "⚠ Failed to remove toolkit (may not exist)" @@ -47,5 +47,3 @@ echo "" echo "==================================================" echo "Cleanup process completed!" echo "Note: Warnings indicate resources that were already removed or never existed." - -# Made with Bob \ No newline at end of file diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh index 7713a08..c957c77 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh @@ -119,8 +119,8 @@ fi # Step 7: Deploy agent echo "" -echo "Step 7: Deploying agent 'autoai_rag_pattern_agent'..." -if orchestrate agents deploy --name autoai_rag_pattern_agent 2>/dev/null; then +echo "Step 7: Deploying agent 'autoai_rag_pattern_agent_v2'..." +if orchestrate agents deploy --name autoai_rag_pattern_agent_v2 2>/dev/null; then echo "✓ Agent deployed successfully" else echo "⚠ Failed to deploy agent (may already be deployed or configuration error)" @@ -135,5 +135,3 @@ echo "To verify the deployment, you can run:" echo " orchestrate agents list" echo " orchestrate toolkits list" echo " orchestrate connections list" - -# Made with Bob \ No newline at end of file diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py index 114c063..27fcbf6 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py @@ -12,10 +12,11 @@ TOOLKIT_PATH = ROOT_DIR / "toolkit.yaml" AGENT_PATH = ROOT_DIR / "agent.yaml" -DEFAULT_TOOLKIT_NAME = "autoai-rag-pattern-toolkit" -DEFAULT_AGENT_NAME = "autoai_rag_pattern_agent" -DEFAULT_TOOL_NAME = "get_rag_pattern_answer" -DEFAULT_SERVER_NAME = "autoai-rag-pattern-toolkit" +DEFAULT_TOOLKIT_NAME = "autoai-rag-pattern-toolkit-v2" +DEFAULT_AGENT_NAME = "autoai_rag_pattern_agent_v2" +DEFAULT_RAG_ANSWER_TOOL_NAME = "get_rag_pattern_answer" +DEFAULT_RAG_DEPLOYMENT_DETAILS_TOOL_NAME = "get_rag_pattern_deployment_details" +DEFAULT_SERVER_NAME = "autoai-rag-pattern-toolkit-v2" DEFAULT_LLM_NAME = "groq/openai/gpt-oss-120b" @@ -64,7 +65,8 @@ def build_metadata( "deployment_id": deployment_id, "deployment_name": deployment_name, "toolkit_name": DEFAULT_TOOLKIT_NAME, - "tool_name": DEFAULT_TOOL_NAME, + "rag_answer_tool_name": DEFAULT_RAG_ANSWER_TOOL_NAME, + "rag_deployment_details_tool_name": DEFAULT_RAG_DEPLOYMENT_DETAILS_TOOL_NAME, "agent_name": DEFAULT_AGENT_NAME, "server_name": DEFAULT_SERVER_NAME, "llm": DEFAULT_LLM_NAME, @@ -74,7 +76,8 @@ def build_metadata( def write_generated_config(metadata: dict[str, Any]) -> None: content = ( f'SERVER_NAME = "{metadata["server_name"]}"\n' - f'TOOL_NAME = "{metadata["tool_name"]}"\n' + f'RAG_ANSWER_TOOL_NAME = "{metadata["rag_answer_tool_name"]}"\n' + f'RAG_DEPLOYMENT_DETAILS_TOOL_NAME = "{metadata["rag_deployment_details_tool_name"]}"\n' f'DEPLOYMENT_NAME = "{metadata["deployment_name"]}"\n' ) with open(GENERATED_CONFIG_PATH, "w", encoding="utf-8") as file: @@ -86,7 +89,7 @@ def build_toolkit_yaml() -> dict[str, Any]: "spec_version": "v1", "kind": "mcp", "name": DEFAULT_TOOLKIT_NAME, - "description": "Generic watsonx.ai AutoAI RAG Pattern toolkit", + "description": "Generic watsonx.ai AutoAI RAG Pattern toolkit with grounded answer and deployment diagnostics tools", "command": "python server.py", "env": [ "WATSONX_URL", @@ -101,14 +104,17 @@ def build_toolkit_yaml() -> dict[str, Any]: def build_agent_yaml(deployment_name: str) -> dict[str, Any]: instructions = ( - f"You are a question-answering assistant powered by the '{deployment_name}' AutoAI RAG Pattern AI service.\n\n" + "You are a knowledge-grounded assistant powered by a watsonx.ai AutoAI RAG Pattern AI service.\n\n" "STRICT RULES — follow these without exception:\n" - "1. NEVER answer questions from your own knowledge or reasoning.\n" - f"2. ALWAYS call {DEFAULT_TOOL_NAME} when the user asks a question.\n" - "3. After the tool returns a result, present the answer clearly to the user.\n" - "4. Do NOT make up answers or use your own knowledge.\n" - "5. If the tool returns an error, inform the user and ask them to try again.\n\n" - "Your role is to pass user questions to the RAG Pattern AI service and relay the responses." + "1. For user questions that require an answer from the knowledge base, ALWAYS call " + f"{DEFAULT_RAG_ANSWER_TOOL_NAME}.\n" + "2. NEVER answer knowledge questions from your own knowledge when the RAG tool should be used.\n" + "3. If the user asks whether the deployment is configured, available, or what deployment is being used, call " + f"{DEFAULT_RAG_DEPLOYMENT_DETAILS_TOOL_NAME}.\n" + "4. After a tool returns a result, summarize it clearly and concisely for the user.\n" + "5. If a tool returns an error, explain the failure plainly and do not invent missing information.\n" + "6. Prefer grounded answers and mention deployment details only when relevant to the user request.\n\n" + "Your role is to route knowledge questions to the RAG Pattern AI service and use the diagnostics tool only for configuration or troubleshooting requests." ) return { @@ -116,13 +122,17 @@ def build_agent_yaml(deployment_name: str) -> dict[str, Any]: "kind": "native", "name": DEFAULT_AGENT_NAME, "description": ( - f"Answers questions using the '{deployment_name}' watsonx.ai deployed AutoAI RAG Pattern AI service." + "Answers knowledge-grounded questions using a watsonx.ai deployed AutoAI " + "RAG Pattern AI service and can inspect deployment configuration for troubleshooting." ), "llm": DEFAULT_LLM_NAME, "style": "react", "hide_reasoning": False, "instructions": instructions, - "tools": [f"{DEFAULT_TOOLKIT_NAME}:{DEFAULT_TOOL_NAME}"], + "tools": [ + f"{DEFAULT_TOOLKIT_NAME}:{DEFAULT_RAG_ANSWER_TOOL_NAME}", + f"{DEFAULT_TOOLKIT_NAME}:{DEFAULT_RAG_DEPLOYMENT_DETAILS_TOOL_NAME}", + ], "collaborators": [], } @@ -173,5 +183,3 @@ def main() -> None: if __name__ == "__main__": main() - -# Made with Bob From d080e15ccf0c8e29a4910e89c130a6d2c975136e Mon Sep 17 00:00:00 2001 From: Karol Zmorski Date: Thu, 18 Jun 2026 15:32:26 +0200 Subject: [PATCH 08/12] Orchestrate agent v3 - improve instructions and add deployment description to it --- .../mcp_server/server.py | 2 + .../mcp_server/utils.py | 4 ++ .../scripts/cleanup.sh | 12 +++--- .../scripts/deploy.sh | 6 +-- .../scripts/generate_template.py | 43 ++++++++++++------- 5 files changed, 42 insertions(+), 25 deletions(-) diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py index a46499e..94d823b 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py @@ -3,6 +3,7 @@ from utils import ( get_deployment_name, + get_deployment_description, get_rag_answer_tool_name, get_rag_deployment_id, get_rag_deployment_details_tool_name, @@ -59,6 +60,7 @@ def get_rag_pattern_deployment_details(): return { "deployment_name": get_deployment_name(), + "deployment_description": get_deployment_description(), "deployment_id": deployment_id, "server_name": get_server_name(), "required_environment_variables": get_required_env_status(), diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py index f82324a..89b9b37 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py @@ -5,6 +5,7 @@ from generated_config import ( DEPLOYMENT_NAME, + DEPLOYMENT_DESCRIPTION, RAG_ANSWER_TOOL_NAME, RAG_DEPLOYMENT_DETAILS_TOOL_NAME, SERVER_NAME, @@ -49,6 +50,9 @@ def get_rag_deployment_details_tool_name() -> str: def get_deployment_name() -> str: return DEPLOYMENT_NAME +def get_deployment_description() -> str: + return DEPLOYMENT_DESCRIPTION + def get_required_env_status() -> dict: load_env() diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh index 1cdb480..45cc8dc 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/cleanup.sh @@ -9,8 +9,8 @@ echo "==================================================" # Step 1: Undeploy the agent echo "" -echo "Step 1: Undeploying agent 'autoai_rag_pattern_agent_v2'..." -if orchestrate agents undeploy --name autoai_rag_pattern_agent_v2 2>/dev/null; then +echo "Step 1: Undeploying agent 'autoai_rag_pattern_agent_v3'..." +if orchestrate agents undeploy --name autoai_rag_pattern_agent_v3 2>/dev/null; then echo "✓ Agent undeployed successfully" else echo "⚠ Failed to undeploy agent (may not exist or already undeployed)" @@ -18,8 +18,8 @@ fi # Step 2: Remove the agent echo "" -echo "Step 2: Removing agent 'autoai_rag_pattern_agent_v2'..." -if orchestrate agents remove --name autoai_rag_pattern_agent_v2 --kind native 2>/dev/null; then +echo "Step 2: Removing agent 'autoai_rag_pattern_agent_v3'..." +if orchestrate agents remove --name autoai_rag_pattern_agent_v3 --kind native 2>/dev/null; then echo "✓ Agent removed successfully" else echo "⚠ Failed to remove agent (may not exist)" @@ -27,8 +27,8 @@ fi # Step 3: Remove the toolkit echo "" -echo "Step 3: Removing toolkit 'autoai-rag-pattern-toolkit-v2'..." -if orchestrate toolkits remove --name autoai-rag-pattern-toolkit-v2 2>/dev/null; then +echo "Step 3: Removing toolkit 'autoai-rag-pattern-toolkit-v3'..." +if orchestrate toolkits remove --name autoai-rag-pattern-toolkit-v3 2>/dev/null; then echo "✓ Toolkit removed successfully" else echo "⚠ Failed to remove toolkit (may not exist)" diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh index c957c77..0daea39 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/deploy.sh @@ -5,7 +5,7 @@ # Each step is wrapped in a try-catch equivalent to handle errors gracefully echo "Starting deployment of AutoAI RAG Pattern orchestration resources..." -echo "=========================================================" +echo "====================================================================" # Check if .env file exists if [ ! -f .env ]; then @@ -119,8 +119,8 @@ fi # Step 7: Deploy agent echo "" -echo "Step 7: Deploying agent 'autoai_rag_pattern_agent_v2'..." -if orchestrate agents deploy --name autoai_rag_pattern_agent_v2 2>/dev/null; then +echo "Step 7: Deploying agent 'autoai_rag_pattern_agent_v3'..." +if orchestrate agents deploy --name autoai_rag_pattern_agent_v3 2>/dev/null; then echo "✓ Agent deployed successfully" else echo "⚠ Failed to deploy agent (may already be deployed or configuration error)" diff --git a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py index 27fcbf6..654a4bc 100644 --- a/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py @@ -5,18 +5,17 @@ from dotenv import load_dotenv from ibm_watsonx_ai import APIClient, Credentials - ROOT_DIR = Path(__file__).resolve().parent.parent MCP_SERVER_DIR = ROOT_DIR / "mcp_server" GENERATED_CONFIG_PATH = MCP_SERVER_DIR / "generated_config.py" TOOLKIT_PATH = ROOT_DIR / "toolkit.yaml" AGENT_PATH = ROOT_DIR / "agent.yaml" -DEFAULT_TOOLKIT_NAME = "autoai-rag-pattern-toolkit-v2" -DEFAULT_AGENT_NAME = "autoai_rag_pattern_agent_v2" +DEFAULT_TOOLKIT_NAME = "autoai-rag-pattern-toolkit-v3" +DEFAULT_AGENT_NAME = "autoai_rag_pattern_agent_v3" DEFAULT_RAG_ANSWER_TOOL_NAME = "get_rag_pattern_answer" DEFAULT_RAG_DEPLOYMENT_DETAILS_TOOL_NAME = "get_rag_pattern_deployment_details" -DEFAULT_SERVER_NAME = "autoai-rag-pattern-toolkit-v2" +DEFAULT_SERVER_NAME = "autoai-rag-pattern-toolkit-v3" DEFAULT_LLM_NAME = "groq/openai/gpt-oss-120b" @@ -48,8 +47,7 @@ def get_deployment_details(client: APIClient, deployment_id: str) -> dict[str, A def get_deployment_name(deployment_details: dict[str, Any]) -> str: - metadata = deployment_details.get("metadata", {}) - name = metadata.get("name") + name = deployment_details.get("metadata", {}).get("name") if not name: raise RuntimeError( "Could not determine deployment name from deployment details" @@ -57,13 +55,21 @@ def get_deployment_name(deployment_details: dict[str, Any]) -> str: return name +def get_deployment_description(deployment_details: dict[str, Any]) -> str: + description = deployment_details.get("metadata", {}).get("description") + + return description if description else "Deployment has no description" + + def build_metadata( - deployment_id: str, - deployment_name: str, + deployment_id: str, + deployment_name: str, + deployment_description: str, ) -> dict[str, Any]: return { "deployment_id": deployment_id, "deployment_name": deployment_name, + "deployment_description": deployment_description, "toolkit_name": DEFAULT_TOOLKIT_NAME, "rag_answer_tool_name": DEFAULT_RAG_ANSWER_TOOL_NAME, "rag_deployment_details_tool_name": DEFAULT_RAG_DEPLOYMENT_DETAILS_TOOL_NAME, @@ -79,6 +85,7 @@ def write_generated_config(metadata: dict[str, Any]) -> None: f'RAG_ANSWER_TOOL_NAME = "{metadata["rag_answer_tool_name"]}"\n' f'RAG_DEPLOYMENT_DETAILS_TOOL_NAME = "{metadata["rag_deployment_details_tool_name"]}"\n' f'DEPLOYMENT_NAME = "{metadata["deployment_name"]}"\n' + f'DEPLOYMENT_DESCRIPTION = "{metadata["deployment_description"]}"\n' ) with open(GENERATED_CONFIG_PATH, "w", encoding="utf-8") as file: file.write(content) @@ -102,18 +109,20 @@ def build_toolkit_yaml() -> dict[str, Any]: } -def build_agent_yaml(deployment_name: str) -> dict[str, Any]: +def build_agent_yaml(deployment_name: str, deployment_description: str) -> dict[str, Any]: instructions = ( "You are a knowledge-grounded assistant powered by a watsonx.ai AutoAI RAG Pattern AI service.\n\n" "STRICT RULES — follow these without exception:\n" "1. For user questions that require an answer from the knowledge base, ALWAYS call " f"{DEFAULT_RAG_ANSWER_TOOL_NAME}.\n" - "2. NEVER answer knowledge questions from your own knowledge when the RAG tool should be used.\n" - "3. If the user asks whether the deployment is configured, available, or what deployment is being used, call " + "2. Determine necessity to use a tool basing on a user request association with the name or description of the RAG Pattern AI service.\n" + f"The name is: '{deployment_name}'. The description is: '{deployment_description}'\n" + "3. If you are not sure, prefer calling " + f"{DEFAULT_RAG_ANSWER_TOOL_NAME}.\n" + "4. NEVER answer knowledge questions from your own knowledge when the RAG tool should be used.\n" + "5. If the user asks whether the deployment is configured, available, or what deployment is being used, call " f"{DEFAULT_RAG_DEPLOYMENT_DETAILS_TOOL_NAME}.\n" - "4. After a tool returns a result, summarize it clearly and concisely for the user.\n" - "5. If a tool returns an error, explain the failure plainly and do not invent missing information.\n" - "6. Prefer grounded answers and mention deployment details only when relevant to the user request.\n\n" + "6. If a tool returns an error, explain the failure plainly and do not invent missing information.\n" "Your role is to route knowledge questions to the RAG Pattern AI service and use the diagnostics tool only for configuration or troubleshooting requests." ) @@ -139,7 +148,7 @@ def build_agent_yaml(deployment_name: str) -> dict[str, Any]: class IndentedListDumper(yaml.SafeDumper): def increase_indent(self, flow: bool = False, indentless: bool = False): - return super().increase_indent(flow=flow, indentless=False) + return super().increase_indent(flow=flow, indentless=indentless) def write_yaml(path: Path, content: dict[str, Any]) -> None: @@ -163,15 +172,17 @@ def main() -> None: deployment_details = get_deployment_details(client, deployment_id) deployment_name = get_deployment_name(deployment_details) + deployment_description = get_deployment_description(deployment_details) metadata = build_metadata( deployment_id=deployment_id, deployment_name=deployment_name, + deployment_description=deployment_description, ) write_generated_config(metadata) toolkit_yaml = build_toolkit_yaml() - agent_yaml = build_agent_yaml(deployment_name=deployment_name) + agent_yaml = build_agent_yaml(deployment_name=deployment_name, deployment_description=deployment_description) write_yaml(TOOLKIT_PATH, toolkit_yaml) write_yaml(AGENT_PATH, agent_yaml) From 717bb60207c6e0cc3880fda2e4b8f24ddc9b1a34 Mon Sep 17 00:00:00 2001 From: Karol Zmorski Date: Mon, 22 Jun 2026 16:06:56 +0200 Subject: [PATCH 09/12] Init commit for implementing MCP orchestrate AI service template - generic --- .../mcp_server/requirements.txt | 5 + .../mcp_server/server.py | 123 +++++++++ .../mcp_server/utils.py | 257 ++++++++++++++++++ .../scripts/cleanup.sh | 49 ++++ .../scripts/deploy.sh | 137 ++++++++++ .../scripts/generate_template.py | 233 ++++++++++++++++ .../template.env | 5 + 7 files changed, 809 insertions(+) create mode 100644 agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/requirements.txt create mode 100644 agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py create mode 100644 agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py create mode 100644 agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh create mode 100644 agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh create mode 100644 agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py create mode 100644 agents/community/mcp-orchestrate-ai-service-template-generic/template.env diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/requirements.txt b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/requirements.txt new file mode 100644 index 0000000..b626e2e --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/requirements.txt @@ -0,0 +1,5 @@ +mcp +python-dotenv +ibm-watsonx-ai +pydantic +pyyaml \ No newline at end of file diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py new file mode 100644 index 0000000..3405e98 --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py @@ -0,0 +1,123 @@ +from mcp.server.fastmcp import FastMCP +from typing import Any, Dict + +from utils import ( + get_deployment_name, + get_deployment_description, + get_tool_name, + get_ai_service_deployment_id, + get_server_name, + prepare_api_client, + get_request_schema, + get_response_schema, + create_pydantic_model_from_schema, + build_payload_from_schema, + extract_output_from_response, +) + +mcp = FastMCP(get_server_name()) + + +def _get_dynamic_input_model(): + """ + Dynamically create the input model based on the deployment's request schema. + Falls back to a simple model if schema is not available. + """ + try: + request_schema = get_request_schema() + if request_schema: + return create_pydantic_model_from_schema(request_schema, "ToolInputSchema") + except Exception as e: + print(f"Warning: Could not create dynamic model from schema: {e}") + + # Fallback to a generic model + from pydantic import BaseModel, Field + + class FallbackInputSchema(BaseModel): + input: str = Field(..., description="Input data for the AI service") + + return FallbackInputSchema + + +def _build_tool_description() -> str: + """ + Build tool description dynamically based on deployment details and schema. + """ + deployment_name = get_deployment_name() + deployment_description = get_deployment_description() + + description = f"Invoke the '{deployment_name}' AI service.\n\n" + + if deployment_description: + description += f"{deployment_description}\n\n" + + try: + request_schema = get_request_schema() + if request_schema and "properties" in request_schema: + description += "Input parameters:\n" + for prop_name, prop_schema in request_schema["properties"].items(): + prop_desc = prop_schema.get("title", "") or prop_schema.get( + "description", "" + ) + prop_type = prop_schema.get("type", "string") + required = prop_name in request_schema.get("required", []) + req_marker = " (required)" if required else " (optional)" + description += f"- {prop_name} ({prop_type}){req_marker}: {prop_desc}\n" + except Exception: + pass + + return description + + +# Create the dynamic input model +ToolInputSchema = _get_dynamic_input_model() + + +@mcp.tool(name=get_tool_name(), description=_build_tool_description()) +def invoke_tool(input_data: ToolInputSchema) -> Dict[str, Any]: + """ + Generic tool that invokes an AI service deployment using dynamic schema-based payload construction. + """ + api_client = prepare_api_client() + deployment_id = get_ai_service_deployment_id() + + # Convert Pydantic model to dict + input_dict = input_data.model_dump() + + # Build payload based on request schema + try: + request_schema = get_request_schema() + payload = ( + build_payload_from_schema(input_dict, request_schema) + if request_schema + else input_dict + ) + except Exception as e: + print(f"Warning: Could not build payload from schema, using raw input: {e}") + payload = input_dict + + # Call the AI service + response = api_client.deployments.run_ai_service(deployment_id, payload) + + # Extract output based on response schema + try: + response_schema = get_response_schema() + output_for_user = ( + extract_output_from_response(response, response_schema) + if response_schema + else response + ) + except Exception as e: + print(f"Warning: Could not extract output from schema, using raw response: {e}") + output_for_user = response + + return { + "input": input_dict, + "output": output_for_user, + # "deployment_name": get_deployment_name(), + # "deployment_description": get_deployment_description(), + } + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py new file mode 100644 index 0000000..e652149 --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py @@ -0,0 +1,257 @@ +import os +from typing import Dict, Any, Optional, Type +from functools import lru_cache + +from dotenv import load_dotenv +from ibm_watsonx_ai import APIClient, Credentials +from pydantic import BaseModel, Field, create_model + +from generated_config import ( + DEPLOYMENT_NAME, + DEPLOYMENT_DESCRIPTION, + SERVER_NAME, + TOOL_NAME, +) + + +def load_env() -> None: + load_dotenv() + + +def prepare_api_client() -> APIClient: + load_env() + return APIClient( + credentials=Credentials( + url=os.getenv("WATSONX_URL"), + api_key=os.getenv("WATSONX_API_KEY"), + ), + space_id=os.getenv("WATSONX_SPACE_ID"), + ) + + +def get_ai_service_deployment_id() -> str: + load_env() + deployment_id = os.getenv("WATSONX_AI_SERVICE_DEPLOYMENT_ID") + if not deployment_id: + raise ValueError("WATSONX_AI_SERVICE_DEPLOYMENT_ID is not set") + return deployment_id + + +def get_server_name() -> str: + return SERVER_NAME + + +def get_tool_name() -> str: + return TOOL_NAME + + +def get_deployment_name() -> str: + return DEPLOYMENT_NAME + + +def get_deployment_description() -> str: + return DEPLOYMENT_DESCRIPTION + + +def get_required_env_status() -> dict: + load_env() + required_env_vars = [ + "WATSONX_URL", + "WATSONX_API_KEY", + "WATSONX_SPACE_ID", + "WATSONX_AI_SERVICE_DEPLOYMENT_ID", + ] + + return {env_var: bool(os.getenv(env_var)) for env_var in required_env_vars} + + +def get_deployment_details(api_client: APIClient) -> Dict[str, Any]: + """ + Fetch deployment details. + """ + deployment_id = get_ai_service_deployment_id() + + try: + details = api_client.deployments.get_details(deployment_id) + return details + except Exception as e: + raise RuntimeError(f"Failed to fetch deployment details: {e}") + + +@lru_cache(maxsize=1) +def get_ai_service_details() -> Dict[str, Any]: + """ + Fetch AI service details including documentation schema. + Cached to avoid repeated API calls. + """ + api_client = prepare_api_client() + deployment_details = get_deployment_details(api_client) + ai_service_id = deployment_details["entity"]["asset"]["id"] + + return api_client.repository.get_ai_service_details(ai_service_id) + + +def get_request_schema() -> Optional[Dict[str, Any]]: + """ + Extract request schema from deployment documentation. + Returns the JSON schema for the request payload. + """ + details = get_ai_service_details() + + try: + documentation = details.get("entity", {}).get("documentation", {}) + request_schema = documentation.get("request", {}).get("application/json", {}) + return request_schema if request_schema else None + except (KeyError, AttributeError): + return None + + +def get_response_schema() -> Optional[Dict[str, Any]]: + """ + Extract response schema from deployment documentation. + Returns the JSON schema for the response payload. + """ + details = get_ai_service_details() + + try: + documentation = details.get("entity", {}).get("documentation", {}) + response_schema = documentation.get("response", {}).get("application/json", {}) + return response_schema if response_schema else None + except (KeyError, AttributeError): + return None + + +def build_payload_from_schema( + input_data: Dict[str, Any], schema: Dict[str, Any] +) -> Dict[str, Any]: + """ + Build API payload from input data based on the request schema. + + Args: + input_data: Dictionary containing user input + schema: JSON schema defining the expected structure + + Returns: + Properly structured payload for the API call + """ + if not schema or "properties" not in schema: + return input_data + + payload = {} + properties = schema.get("properties", {}) + + for prop_name, prop_schema in properties.items(): + if prop_name in input_data: + payload[prop_name] = input_data[prop_name] + + return payload + + +def extract_output_from_response( + response: Dict[str, Any], schema: Dict[str, Any] +) -> Any: + """ + Extract relevant output from API response based on the response schema. + + Args: + response: Raw API response + schema: JSON schema defining the response structure + + Returns: + Extracted output value(s) + """ + if not schema or "properties" not in schema: + return response + + properties = schema.get("properties", {}) + + # Try to extract the main content based on common patterns + # For chat-like responses, look for choices[0].message.content + if "choices" in properties and "choices" in response: + choices = response.get("choices", []) + if choices and len(choices) > 0: + message = choices[0].get("message", {}) + if "content" in message: + return message["content"] + # Handle streaming delta format + if "delta" in message: + delta = message.get("delta", {}) + if "content" in delta: + return delta["content"] + + # For simpler responses, return the first property value + for prop_name in properties.keys(): + if prop_name in response: + return response[prop_name] + + # Fallback to returning the entire response + return response + + +def create_pydantic_model_from_schema( + schema: Dict[str, Any], model_name: str = "DynamicModel" +) -> Type[BaseModel]: + """ + Create a Pydantic model dynamically from a JSON schema. + + Args: + schema: JSON schema dictionary + model_name: Name for the generated model + + Returns: + Dynamically created Pydantic model class + """ + if not schema or "properties" not in schema: + # Return a simple model with a generic field + return create_model( + model_name, input=(str, Field(..., description="Generic input")) + ) + + properties = schema.get("properties", {}) + required_fields = set(schema.get("required", [])) + field_definitions = {} + + for prop_name, prop_schema in properties.items(): + field_type = _json_schema_type_to_python(prop_schema) + field_description = prop_schema.get("title", "") or prop_schema.get( + "description", "" + ) + is_required = prop_name in required_fields + + field_definitions[prop_name] = ( + field_type, + Field(... if is_required else None, description=field_description), + ) + + return create_model(model_name, **field_definitions) + + +def _json_schema_type_to_python(prop_schema: Dict[str, Any]) -> type: + """ + Convert JSON schema type to Python type for Pydantic. + + Args: + prop_schema: Property schema dictionary + + Returns: + Python type + """ + schema_type = prop_schema.get("type", "string") + + # Handle array types + if schema_type == "array": + items_schema = prop_schema.get("items", {}) + item_type = _json_schema_type_to_python(items_schema) + return list[item_type] + + # Handle basic types + type_mapping = { + "string": str, + "integer": int, + "number": float, + "boolean": bool, + "object": dict, + "null": type(None), + } + + return type_mapping.get(schema_type, str) diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh new file mode 100644 index 0000000..45cc8dc --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Cleanup script for AutoAI RAG Pattern orchestration resources +# This script removes all created items with individual error handling +# Each deletion is wrapped in a try-catch equivalent to handle non-existent resources + +echo "Starting cleanup of AutoAI RAG Pattern orchestration resources..." +echo "==================================================" + +# Step 1: Undeploy the agent +echo "" +echo "Step 1: Undeploying agent 'autoai_rag_pattern_agent_v3'..." +if orchestrate agents undeploy --name autoai_rag_pattern_agent_v3 2>/dev/null; then + echo "✓ Agent undeployed successfully" +else + echo "⚠ Failed to undeploy agent (may not exist or already undeployed)" +fi + +# Step 2: Remove the agent +echo "" +echo "Step 2: Removing agent 'autoai_rag_pattern_agent_v3'..." +if orchestrate agents remove --name autoai_rag_pattern_agent_v3 --kind native 2>/dev/null; then + echo "✓ Agent removed successfully" +else + echo "⚠ Failed to remove agent (may not exist)" +fi + +# Step 3: Remove the toolkit +echo "" +echo "Step 3: Removing toolkit 'autoai-rag-pattern-toolkit-v3'..." +if orchestrate toolkits remove --name autoai-rag-pattern-toolkit-v3 2>/dev/null; then + echo "✓ Toolkit removed successfully" +else + echo "⚠ Failed to remove toolkit (may not exist)" +fi + +# Step 4: Remove the connection +echo "" +echo "Step 4: Removing connection 'autoai-rag-pattern-connection'..." +if orchestrate connections remove --app-id autoai-rag-pattern-connection 2>/dev/null; then + echo "✓ Connection removed successfully" +else + echo "⚠ Failed to remove connection (may not exist)" +fi + +echo "" +echo "==================================================" +echo "Cleanup process completed!" +echo "Note: Warnings indicate resources that were already removed or never existed." diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh new file mode 100644 index 0000000..0daea39 --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh @@ -0,0 +1,137 @@ +#!/bin/bash + +# Deployment script for AutoAI RAG Pattern orchestration resources +# This script creates and configures all required resources with individual error handling +# Each step is wrapped in a try-catch equivalent to handle errors gracefully + +echo "Starting deployment of AutoAI RAG Pattern orchestration resources..." +echo "====================================================================" + +# Check if .env file exists +if [ ! -f .env ]; then + echo "❌ Error: .env file not found in current directory" + echo "Please create a .env file with required environment variables" + exit 1 +fi + +# Step 1: Generate template files (toolkit.yaml, agent.yaml, generated_config.py) +echo "" +echo "Step 1: Generating template files from AutoAI RAG Pattern deployment..." +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if python "$SCRIPT_DIR/generate_template.py" 2>/dev/null; then + echo "✓ Template files generated successfully" + echo " - toolkit.yaml" + echo " - agent.yaml" + echo " - mcp_server/generated_config.py" +else + echo "❌ Failed to generate template files" + echo "Please ensure:" + echo " - Python dependencies are installed (ibm-watsonx-ai, python-dotenv, pyyaml)" + echo " - .env file contains valid credentials" + echo " - WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID is correct" + exit 1 +fi + +# Step 2: Add connection +echo "" +echo "Step 2: Adding connection 'autoai-rag-pattern-connection'..." +if orchestrate connections add -a autoai-rag-pattern-connection 2>/dev/null; then + echo "✓ Connection added successfully" +else + echo "⚠ Failed to add connection (may already exist)" +fi + +# Step 3: Export environment variables from .env file +echo "" +echo "Step 3: Loading environment variables from .env file..." +if export $(grep -v '^#' .env | xargs) 2>/dev/null; then + echo "✓ Environment variables loaded successfully" + echo " - WATSONX_URL: ${WATSONX_URL:0:30}..." + echo " - WATSONX_API_KEY: ${WATSONX_API_KEY:0:10}..." + echo " - WATSONX_SPACE_ID: $WATSONX_SPACE_ID" + echo " - WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID: $WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID" +else + echo "❌ Failed to load environment variables from .env file" + exit 1 +fi + +# Step 4: Configure connections for both draft and live environments +echo "" +echo "Step 4: Configuring connections for draft and live environments..." + +for env in draft live; do + echo "" + echo " Configuring $env environment..." + + # Configure connection + if orchestrate connections configure \ + -a autoai-rag-pattern-connection \ + --env $env \ + --type team \ + --kind key_value 2>/dev/null; then + echo " ✓ Connection configured for $env environment" + else + echo " ⚠ Failed to configure connection for $env environment" + continue + fi + + # Set credentials + if orchestrate connections set-credentials \ + -a autoai-rag-pattern-connection \ + --env $env \ + -e "WATSONX_URL=$WATSONX_URL" \ + -e "WATSONX_API_KEY=$WATSONX_API_KEY" \ + -e "WATSONX_SPACE_ID=$WATSONX_SPACE_ID" \ + -e "WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID=$WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID" 2>/dev/null; then + echo " ✓ Credentials set for $env environment" + else + echo " ⚠ Failed to set credentials for $env environment" + fi +done + +# Step 5: Import toolkit +echo "" +echo "Step 5: Importing toolkit from toolkit.yaml..." +if [ ! -f toolkit.yaml ]; then + echo "❌ Error: toolkit.yaml file not found in current directory" + exit 1 +fi + +if orchestrate toolkits import -f toolkit.yaml -a autoai-rag-pattern-connection 2>/dev/null; then + echo "✓ Toolkit imported successfully" +else + echo "⚠ Failed to import toolkit (may already exist or invalid configuration)" +fi + +# Step 6: Import agent +echo "" +echo "Step 6: Importing agent from agent.yaml..." +if [ ! -f agent.yaml ]; then + echo "❌ Error: agent.yaml file not found in current directory" + exit 1 +fi + +if orchestrate agents import -f agent.yaml 2>/dev/null; then + echo "✓ Agent imported successfully" +else + echo "⚠ Failed to import agent (may already exist or invalid configuration)" +fi + +# Step 7: Deploy agent +echo "" +echo "Step 7: Deploying agent 'autoai_rag_pattern_agent_v3'..." +if orchestrate agents deploy --name autoai_rag_pattern_agent_v3 2>/dev/null; then + echo "✓ Agent deployed successfully" +else + echo "⚠ Failed to deploy agent (may already be deployed or configuration error)" +fi + +echo "" +echo "=========================================================" +echo "Deployment process completed!" +echo "Note: Warnings indicate resources that already exist or configuration issues." +echo "" +echo "To verify the deployment, you can run:" +echo " orchestrate agents list" +echo " orchestrate toolkits list" +echo " orchestrate connections list" diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py new file mode 100644 index 0000000..6f91182 --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py @@ -0,0 +1,233 @@ +from pathlib import Path +from typing import Any + +import yaml +from dotenv import load_dotenv +from ibm_watsonx_ai import APIClient, Credentials + +ROOT_DIR = Path(__file__).resolve().parent.parent +MCP_SERVER_DIR = ROOT_DIR / "mcp_server" +GENERATED_CONFIG_PATH = MCP_SERVER_DIR / "generated_config.py" +TOOLKIT_PATH = ROOT_DIR / "toolkit.yaml" +AGENT_PATH = ROOT_DIR / "agent.yaml" + +DEFAULT_TOOLKIT_NAME = "ai-services-toolkit-v1" +DEFAULT_AGENT_NAME = "ai_services_agent_v1" +DEFAULT_TOOL_NAME = "get_ai_service_response" +DEFAULT_SERVER_NAME = DEFAULT_TOOLKIT_NAME +DEFAULT_LLM_NAME = "groq/openai/gpt-oss-120b" + + +def load_env() -> None: + load_dotenv(ROOT_DIR / ".env") + + +def prepare_api_client() -> APIClient: + return APIClient( + credentials=Credentials( + url=require_env("WATSONX_URL"), + api_key=require_env("WATSONX_API_KEY"), + ), + space_id=require_env("WATSONX_SPACE_ID"), + ) + + +def require_env(name: str) -> str: + import os + + value = os.getenv(name) + if not value: + raise ValueError(f"{name} is not set") + return value + + +def get_deployment_details(client: APIClient, deployment_id: str) -> dict[str, Any]: + return client.deployments.get_details(deployment_id) + + +def get_deployment_name(deployment_details: dict[str, Any]) -> str: + name = deployment_details.get("metadata", {}).get("name") + if not name: + raise RuntimeError( + "Could not determine deployment name from deployment details" + ) + return name + + +def get_deployment_description(deployment_details: dict[str, Any]) -> str: + description = deployment_details.get("metadata", {}).get("description") + + return description if description else "Deployment has no description" + + +def get_ai_service_id(deployment_details: dict[str, Any]) -> str: + ai_service_id = deployment_details["entity"]["asset"]["id"] + + return ai_service_id + + +def get_ai_service_details(client: APIClient, ai_service_id: str) -> dict[str, Any]: + return client.repository.get_ai_service_details(ai_service_id) + + +def get_ai_service_name(ai_service_details: dict[str, Any]) -> str: + name = ai_service_details.get("metadata", {}).get("name") + if not name: + raise RuntimeError( + "Could not determine AI service name from AI service details" + ) + return name + + +def get_ai_service_description(ai_service_details: dict[str, Any]) -> str: + description = ai_service_details.get("metadata", {}).get("description") + + return description if description else "AI service has no description" + + +def build_metadata( + deployment_id: str, + deployment_name: str, + deployment_description: str, + ai_service_id: str, + ai_service_name: str, + ai_service_description: str, +) -> dict[str, Any]: + return { + "deployment_id": deployment_id, + "deployment_name": deployment_name, + "deployment_description": deployment_description, + "ai_service_id": ai_service_id, + "ai_service_name": ai_service_name, + "ai_service_description": ai_service_description, + "toolkit_name": DEFAULT_TOOLKIT_NAME, + "tool_name": DEFAULT_TOOL_NAME, + "agent_name": DEFAULT_AGENT_NAME, + "server_name": DEFAULT_SERVER_NAME, + "llm": DEFAULT_LLM_NAME, + } + + +def write_generated_config(metadata: dict[str, Any]) -> None: + content = ( + f'SERVER_NAME = "{metadata["server_name"]}"\n' + f'TOOL_NAME = "{metadata["tool_name"]}"\n' + f'DEPLOYMENT_NAME = "{metadata["deployment_name"]}"\n' + f'DEPLOYMENT_DESCRIPTION = "{metadata["deployment_description"]}"\n' + f'AI_SERVICE_ID = "{metadata["ai_service_id"]}"\n' + f'AI_SERVICE_NAME = "{metadata["ai_service_name"]}"\n' + f'AI_SERVICE_DESCRIPTION = "{metadata["ai_service_description"]}"\n' + ) + with open(GENERATED_CONFIG_PATH, "w", encoding="utf-8") as file: + file.write(content) + + +def build_toolkit_yaml() -> dict[str, Any]: + return { + "spec_version": "v1", + "kind": "mcp", + "name": DEFAULT_TOOLKIT_NAME, + "description": "Generic watsonx.ai AI Services toolkit with deployed AI services as tools", + "command": "python server.py", + "env": [ + "WATSONX_URL", + "WATSONX_API_KEY", + "WATSONX_SPACE_ID", + "WATSONX_AI_SERVICE_DEPLOYMENT_ID", + ], + "tools": ["*"], + "package_root": "./mcp_server", + } + + +def build_agent_yaml(deployment_name: str, deployment_description: str) -> dict[str, Any]: + instructions = ( + "You are an assistant powered by deployed watsonx.ai AI services.\n\n" + "STRICT RULES — follow these without exception:\n" + "1. For user questions that require an answer from the knowledge base, ALWAYS call " + f"{DEFAULT_TOOL_NAME}.\n" + "2. Determine necessity to use a tool basing on a user request association with the name or description of the RAG Pattern AI service.\n" + f"The name is: '{deployment_name}'. The description is: '{deployment_description}'\n" + "3. If you are not sure, prefer calling " + f"{DEFAULT_TOOL_NAME}.\n" + "4. NEVER answer knowledge questions from your own knowledge when the RAG tool should be used.\n" + "5. If the user asks whether the deployment is configured, available, or what deployment is being used, call " + f"{DEFAULT_RAG_DEPLOYMENT_DETAILS_TOOL_NAME}.\n" + "6. If a tool returns an error, explain the failure plainly and do not invent missing information.\n" + "Your role is to route knowledge questions to the RAG Pattern AI service and use the diagnostics tool only for configuration or troubleshooting requests." + ) + + return { + "spec_version": "v1", + "kind": "native", + "name": DEFAULT_AGENT_NAME, + "description": ( + "Answers user questions using a watsonx.ai deployed AI service if needed." + ), + "llm": DEFAULT_LLM_NAME, + "style": "react", + "hide_reasoning": False, + "instructions": instructions, + "tools": [ + f"{DEFAULT_TOOLKIT_NAME}:{DEFAULT_TOOL_NAME}", + ], + "collaborators": [], + } + + +class IndentedListDumper(yaml.SafeDumper): + def increase_indent(self, flow: bool = False, indentless: bool = False): + return super().increase_indent(flow=flow, indentless=indentless) + + +def write_yaml(path: Path, content: dict[str, Any]) -> None: + with open(path, "w", encoding="utf-8") as file: + yaml.dump( + content, + file, + Dumper=IndentedListDumper, + sort_keys=False, + allow_unicode=True, + default_flow_style=False, + indent=2, + ) + + +def main() -> None: + load_env() + client = prepare_api_client() + + deployment_id = require_env("WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID") + + deployment_details = get_deployment_details(client, deployment_id) + deployment_name = get_deployment_name(deployment_details) + deployment_description = get_deployment_description(deployment_details) + + ai_service_id = get_ai_service_id(deployment_details) + ai_service_details = get_ai_service_details(client, ai_service_id) + ai_service_name = get_ai_service_name(ai_service_details) + ai_service_description = get_ai_service_description(ai_service_details) + + metadata = build_metadata( + deployment_id=deployment_id, + deployment_name=deployment_name, + deployment_description=deployment_description, + ai_service_id=ai_service_id, + ai_service_name=ai_service_name, + ai_service_description=ai_service_description, + ) + write_generated_config(metadata) + + toolkit_yaml = build_toolkit_yaml() + agent_yaml = build_agent_yaml(deployment_name=deployment_name, deployment_description=deployment_description) + + write_yaml(TOOLKIT_PATH, toolkit_yaml) + write_yaml(AGENT_PATH, agent_yaml) + + print(f"Generated {TOOLKIT_PATH}") + print(f"Generated {AGENT_PATH}") + print(f"Generated {GENERATED_CONFIG_PATH}") + + +if __name__ == "__main__": + main() diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/template.env b/agents/community/mcp-orchestrate-ai-service-template-generic/template.env new file mode 100644 index 0000000..4316c99 --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/template.env @@ -0,0 +1,5 @@ +WATSONX_URL=https://us-south.ml.cloud.ibm.com +WATSONX_API_KEY=your_api_key +WATSONX_SPACE_ID=your_space_id + +WATSONX_AI_SERVICE_DEPLOYMENT_ID=your_autoai_rag_pattern_deployment_id \ No newline at end of file From b50703e8b40c7c786bea5493980957034d1b4988 Mon Sep 17 00:00:00 2001 From: Karol Zmorski Date: Tue, 23 Jun 2026 16:39:26 +0200 Subject: [PATCH 10/12] Generic solution done for AI service agent template - V1 --- .../mcp_server/server.py | 57 +++++++++---------- .../mcp_server/utils.py | 21 +++++-- .../scripts/cleanup.sh | 22 +++---- .../scripts/deploy.sh | 26 ++++----- .../scripts/generate_template.py | 47 +++++++++------ 5 files changed, 97 insertions(+), 76 deletions(-) diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py index 3405e98..9777e7a 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py @@ -51,20 +51,20 @@ def _build_tool_description() -> str: if deployment_description: description += f"{deployment_description}\n\n" - try: - request_schema = get_request_schema() - if request_schema and "properties" in request_schema: - description += "Input parameters:\n" - for prop_name, prop_schema in request_schema["properties"].items(): - prop_desc = prop_schema.get("title", "") or prop_schema.get( - "description", "" - ) - prop_type = prop_schema.get("type", "string") - required = prop_name in request_schema.get("required", []) - req_marker = " (required)" if required else " (optional)" - description += f"- {prop_name} ({prop_type}){req_marker}: {prop_desc}\n" - except Exception: - pass + # try: + # request_schema = get_request_schema() + # if request_schema and "properties" in request_schema: + # description += "Input parameters:\n" + # for prop_name, prop_schema in request_schema["properties"].items(): + # prop_desc = prop_schema.get("title", "") or prop_schema.get( + # "description", "" + # ) + # prop_type = prop_schema.get("type", "string") + # required = prop_name in request_schema.get("required", []) + # req_marker = " (required)" if required else " (optional)" + # description += f"- {prop_name} ({prop_type}){req_marker}: {prop_desc}\n" + # except Exception: + # pass return description @@ -100,23 +100,18 @@ def invoke_tool(input_data: ToolInputSchema) -> Dict[str, Any]: response = api_client.deployments.run_ai_service(deployment_id, payload) # Extract output based on response schema - try: - response_schema = get_response_schema() - output_for_user = ( - extract_output_from_response(response, response_schema) - if response_schema - else response - ) - except Exception as e: - print(f"Warning: Could not extract output from schema, using raw response: {e}") - output_for_user = response - - return { - "input": input_dict, - "output": output_for_user, - # "deployment_name": get_deployment_name(), - # "deployment_description": get_deployment_description(), - } + # try: + # response_schema = get_response_schema() + # output = ( + # extract_output_from_response(response, response_schema) + # if response_schema + # else response + # ) + # except Exception as e: + # print(f"Warning: Could not extract output from schema, using raw response: {e}") + # output = response + + return response if __name__ == "__main__": diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py index e652149..2bac855 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py @@ -193,6 +193,7 @@ def create_pydantic_model_from_schema( ) -> Type[BaseModel]: """ Create a Pydantic model dynamically from a JSON schema. + Recursively handles nested objects and arrays. Args: schema: JSON schema dictionary @@ -212,7 +213,9 @@ def create_pydantic_model_from_schema( field_definitions = {} for prop_name, prop_schema in properties.items(): - field_type = _json_schema_type_to_python(prop_schema) + # Create a unique model name for nested structures + nested_model_name = f"{model_name}_{prop_name.capitalize()}" + field_type = _json_schema_type_to_python(prop_schema, nested_model_name) field_description = prop_schema.get("title", "") or prop_schema.get( "description", "" ) @@ -226,31 +229,39 @@ def create_pydantic_model_from_schema( return create_model(model_name, **field_definitions) -def _json_schema_type_to_python(prop_schema: Dict[str, Any]) -> type: +def _json_schema_type_to_python( + prop_schema: Dict[str, Any], model_name: str = "NestedModel" +) -> type: """ Convert JSON schema type to Python type for Pydantic. + Recursively creates nested Pydantic models for object types with properties. Args: prop_schema: Property schema dictionary + model_name: Name for nested model (used for object types) Returns: - Python type + Python type or Pydantic model class """ schema_type = prop_schema.get("type", "string") # Handle array types if schema_type == "array": items_schema = prop_schema.get("items", {}) - item_type = _json_schema_type_to_python(items_schema) + item_type = _json_schema_type_to_python(items_schema, f"{model_name}Item") return list[item_type] + # Handle object types with properties - create nested Pydantic model + if schema_type == "object" and "properties" in prop_schema: + return create_pydantic_model_from_schema(prop_schema, model_name) + # Handle basic types type_mapping = { "string": str, "integer": int, "number": float, "boolean": bool, - "object": dict, + "object": dict, # Fallback for objects without properties "null": type(None), } diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh index 45cc8dc..0f8ee9a 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh @@ -1,16 +1,16 @@ #!/bin/bash -# Cleanup script for AutoAI RAG Pattern orchestration resources +# Cleanup script for AI Service orchestration resources # This script removes all created items with individual error handling # Each deletion is wrapped in a try-catch equivalent to handle non-existent resources -echo "Starting cleanup of AutoAI RAG Pattern orchestration resources..." -echo "==================================================" +echo "Starting cleanup of AI Service orchestration resources..." +echo "=========================================================" # Step 1: Undeploy the agent echo "" -echo "Step 1: Undeploying agent 'autoai_rag_pattern_agent_v3'..." -if orchestrate agents undeploy --name autoai_rag_pattern_agent_v3 2>/dev/null; then +echo "Step 1: Undeploying agent 'ai_services_agent_v1'..." +if orchestrate agents undeploy --name ai_services_agent_v1 2>/dev/null; then echo "✓ Agent undeployed successfully" else echo "⚠ Failed to undeploy agent (may not exist or already undeployed)" @@ -18,8 +18,8 @@ fi # Step 2: Remove the agent echo "" -echo "Step 2: Removing agent 'autoai_rag_pattern_agent_v3'..." -if orchestrate agents remove --name autoai_rag_pattern_agent_v3 --kind native 2>/dev/null; then +echo "Step 2: Removing agent 'ai_services_agent_v1'..." +if orchestrate agents remove --name ai_services_agent_v1 --kind native 2>/dev/null; then echo "✓ Agent removed successfully" else echo "⚠ Failed to remove agent (may not exist)" @@ -27,8 +27,8 @@ fi # Step 3: Remove the toolkit echo "" -echo "Step 3: Removing toolkit 'autoai-rag-pattern-toolkit-v3'..." -if orchestrate toolkits remove --name autoai-rag-pattern-toolkit-v3 2>/dev/null; then +echo "Step 3: Removing toolkit 'ai-services-toolkit-v1'..." +if orchestrate toolkits remove --name ai-services-toolkit-v1 2>/dev/null; then echo "✓ Toolkit removed successfully" else echo "⚠ Failed to remove toolkit (may not exist)" @@ -36,8 +36,8 @@ fi # Step 4: Remove the connection echo "" -echo "Step 4: Removing connection 'autoai-rag-pattern-connection'..." -if orchestrate connections remove --app-id autoai-rag-pattern-connection 2>/dev/null; then +echo "Step 4: Removing connection 'ai-service-connection'..." +if orchestrate connections remove --app-id ai-service-connection 2>/dev/null; then echo "✓ Connection removed successfully" else echo "⚠ Failed to remove connection (may not exist)" diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh index 0daea39..91e87ce 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh @@ -1,10 +1,10 @@ #!/bin/bash -# Deployment script for AutoAI RAG Pattern orchestration resources +# Deployment script for AI Service orchestration resources # This script creates and configures all required resources with individual error handling # Each step is wrapped in a try-catch equivalent to handle errors gracefully -echo "Starting deployment of AutoAI RAG Pattern orchestration resources..." +echo "Starting deployment of AI Service orchestration resources..." echo "====================================================================" # Check if .env file exists @@ -16,7 +16,7 @@ fi # Step 1: Generate template files (toolkit.yaml, agent.yaml, generated_config.py) echo "" -echo "Step 1: Generating template files from AutoAI RAG Pattern deployment..." +echo "Step 1: Generating template files from AI Service deployment..." SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if python "$SCRIPT_DIR/generate_template.py" 2>/dev/null; then echo "✓ Template files generated successfully" @@ -28,14 +28,14 @@ else echo "Please ensure:" echo " - Python dependencies are installed (ibm-watsonx-ai, python-dotenv, pyyaml)" echo " - .env file contains valid credentials" - echo " - WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID is correct" + echo " - WATSONX_AI_SERVICE_DEPLOYMENT_ID is correct" exit 1 fi # Step 2: Add connection echo "" -echo "Step 2: Adding connection 'autoai-rag-pattern-connection'..." -if orchestrate connections add -a autoai-rag-pattern-connection 2>/dev/null; then +echo "Step 2: Adding connection 'ai-service-connection'..." +if orchestrate connections add -a ai-service-connection 2>/dev/null; then echo "✓ Connection added successfully" else echo "⚠ Failed to add connection (may already exist)" @@ -49,7 +49,7 @@ if export $(grep -v '^#' .env | xargs) 2>/dev/null; then echo " - WATSONX_URL: ${WATSONX_URL:0:30}..." echo " - WATSONX_API_KEY: ${WATSONX_API_KEY:0:10}..." echo " - WATSONX_SPACE_ID: $WATSONX_SPACE_ID" - echo " - WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID: $WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID" + echo " - WATSONX_AI_SERVICE_DEPLOYMENT_ID: $WATSONX_AI_SERVICE_DEPLOYMENT_ID" else echo "❌ Failed to load environment variables from .env file" exit 1 @@ -65,7 +65,7 @@ for env in draft live; do # Configure connection if orchestrate connections configure \ - -a autoai-rag-pattern-connection \ + -a ai-service-connection \ --env $env \ --type team \ --kind key_value 2>/dev/null; then @@ -77,12 +77,12 @@ for env in draft live; do # Set credentials if orchestrate connections set-credentials \ - -a autoai-rag-pattern-connection \ + -a ai-service-connection \ --env $env \ -e "WATSONX_URL=$WATSONX_URL" \ -e "WATSONX_API_KEY=$WATSONX_API_KEY" \ -e "WATSONX_SPACE_ID=$WATSONX_SPACE_ID" \ - -e "WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID=$WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID" 2>/dev/null; then + -e "WATSONX_AI_SERVICE_DEPLOYMENT_ID=$WATSONX_AI_SERVICE_DEPLOYMENT_ID" 2>/dev/null; then echo " ✓ Credentials set for $env environment" else echo " ⚠ Failed to set credentials for $env environment" @@ -97,7 +97,7 @@ if [ ! -f toolkit.yaml ]; then exit 1 fi -if orchestrate toolkits import -f toolkit.yaml -a autoai-rag-pattern-connection 2>/dev/null; then +if orchestrate toolkits import -f toolkit.yaml -a ai-service-connection 2>/dev/null; then echo "✓ Toolkit imported successfully" else echo "⚠ Failed to import toolkit (may already exist or invalid configuration)" @@ -119,8 +119,8 @@ fi # Step 7: Deploy agent echo "" -echo "Step 7: Deploying agent 'autoai_rag_pattern_agent_v3'..." -if orchestrate agents deploy --name autoai_rag_pattern_agent_v3 2>/dev/null; then +echo "Step 7: Deploying agent 'ai_services_agent_v1'..." +if orchestrate agents deploy --name ai_services_agent_v1 2>/dev/null; then echo "✓ Agent deployed successfully" else echo "⚠ Failed to deploy agent (may already be deployed or configuration error)" diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py index 6f91182..2343b96 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py @@ -127,7 +127,7 @@ def build_toolkit_yaml() -> dict[str, Any]: "spec_version": "v1", "kind": "mcp", "name": DEFAULT_TOOLKIT_NAME, - "description": "Generic watsonx.ai AI Services toolkit with deployed AI services as tools", + "description": "Generic watsonx.ai AI Services toolkit with deployed AI service as a tool", "command": "python server.py", "env": [ "WATSONX_URL", @@ -140,21 +140,31 @@ def build_toolkit_yaml() -> dict[str, Any]: } -def build_agent_yaml(deployment_name: str, deployment_description: str) -> dict[str, Any]: +def build_agent_yaml( + deployment_name: str, + deployment_description: str, + ai_service_name: str, + ai_service_description: str, +) -> dict[str, Any]: instructions = ( "You are an assistant powered by deployed watsonx.ai AI services.\n\n" + "AVAILABLE AI SERVICE:\n" + f"- Deployment Name: {deployment_name}\n" + f"- Deployment Description: {deployment_description}\n" + f"- AI Service Name: {ai_service_name}\n" + f"- AI Service Description: {ai_service_description}\n\n" "STRICT RULES — follow these without exception:\n" - "1. For user questions that require an answer from the knowledge base, ALWAYS call " - f"{DEFAULT_TOOL_NAME}.\n" - "2. Determine necessity to use a tool basing on a user request association with the name or description of the RAG Pattern AI service.\n" - f"The name is: '{deployment_name}'. The description is: '{deployment_description}'\n" - "3. If you are not sure, prefer calling " - f"{DEFAULT_TOOL_NAME}.\n" - "4. NEVER answer knowledge questions from your own knowledge when the RAG tool should be used.\n" - "5. If the user asks whether the deployment is configured, available, or what deployment is being used, call " - f"{DEFAULT_RAG_DEPLOYMENT_DETAILS_TOOL_NAME}.\n" - "6. If a tool returns an error, explain the failure plainly and do not invent missing information.\n" - "Your role is to route knowledge questions to the RAG Pattern AI service and use the diagnostics tool only for configuration or troubleshooting requests." + "1. When the user says hello or asks what you can do, list the available tools and briefly explain their purpose and required inputs.\n" + "2. ONLY call the AI service tool when the user's request is clearly related to the deployment or AI service based on their names and descriptions above.\n" + "3. If one or more required tool input fields are missing, ask only for the missing fields.\n" + "4. Analyze the user's request carefully:\n" + " - If it matches the purpose described in the deployment/service descriptions, use the tool\n" + " - If it's a general question unrelated to the AI service's purpose, answer from your own knowledge\n" + " - When in doubt about relevance, ask the user for clarification rather than calling the tool\n" + "5. If a tool returns an error, explain the failure clearly and do not invent missing information.\n" + "6. When the tool returns a result, retrieve only helpful data based on a user's request and provide them to the user.\n" + "7. Be helpful and conversational, but precise about when to use the AI service tool.\n\n" + "Your role is to intelligently route requests to the AI service only when appropriate, based on the service's documented purpose." ) return { @@ -162,7 +172,7 @@ def build_agent_yaml(deployment_name: str, deployment_description: str) -> dict[ "kind": "native", "name": DEFAULT_AGENT_NAME, "description": ( - "Answers user questions using a watsonx.ai deployed AI service if needed." + "Answers user questions using a watsonx.ai deployed AI service when appropriate." ), "llm": DEFAULT_LLM_NAME, "style": "react", @@ -197,7 +207,7 @@ def main() -> None: load_env() client = prepare_api_client() - deployment_id = require_env("WATSONX_AUTOAI_RAG_PATTERN_DEPLOYMENT_ID") + deployment_id = require_env("WATSONX_AI_SERVICE_DEPLOYMENT_ID") deployment_details = get_deployment_details(client, deployment_id) deployment_name = get_deployment_name(deployment_details) @@ -219,7 +229,12 @@ def main() -> None: write_generated_config(metadata) toolkit_yaml = build_toolkit_yaml() - agent_yaml = build_agent_yaml(deployment_name=deployment_name, deployment_description=deployment_description) + agent_yaml = build_agent_yaml( + deployment_name=deployment_name, + deployment_description=deployment_description, + ai_service_name=ai_service_name, + ai_service_description=ai_service_description, + ) write_yaml(TOOLKIT_PATH, toolkit_yaml) write_yaml(AGENT_PATH, agent_yaml) From 9c2c9719d5c8e74da98a9d225c48d0418c09fdd8 Mon Sep 17 00:00:00 2001 From: Karol Zmorski Date: Fri, 3 Jul 2026 12:08:34 +0200 Subject: [PATCH 11/12] V2 for AI Service which answers questions relating to a given image --- .../scripts/cleanup.sh | 12 ++++++------ .../scripts/deploy.sh | 4 ++-- .../scripts/generate_template.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh index 0f8ee9a..51f767c 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh @@ -9,8 +9,8 @@ echo "=========================================================" # Step 1: Undeploy the agent echo "" -echo "Step 1: Undeploying agent 'ai_services_agent_v1'..." -if orchestrate agents undeploy --name ai_services_agent_v1 2>/dev/null; then +echo "Step 1: Undeploying agent 'ai_services_agent_v2'..." +if orchestrate agents undeploy --name ai_services_agent_v2 2>/dev/null; then echo "✓ Agent undeployed successfully" else echo "⚠ Failed to undeploy agent (may not exist or already undeployed)" @@ -18,8 +18,8 @@ fi # Step 2: Remove the agent echo "" -echo "Step 2: Removing agent 'ai_services_agent_v1'..." -if orchestrate agents remove --name ai_services_agent_v1 --kind native 2>/dev/null; then +echo "Step 2: Removing agent 'ai_services_agent_v2'..." +if orchestrate agents remove --name ai_services_agent_v2 --kind native 2>/dev/null; then echo "✓ Agent removed successfully" else echo "⚠ Failed to remove agent (may not exist)" @@ -27,8 +27,8 @@ fi # Step 3: Remove the toolkit echo "" -echo "Step 3: Removing toolkit 'ai-services-toolkit-v1'..." -if orchestrate toolkits remove --name ai-services-toolkit-v1 2>/dev/null; then +echo "Step 3: Removing toolkit 'ai-services-toolkit-v2'..." +if orchestrate toolkits remove --name ai-services-toolkit-v2 2>/dev/null; then echo "✓ Toolkit removed successfully" else echo "⚠ Failed to remove toolkit (may not exist)" diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh index 91e87ce..8fa55bf 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh @@ -119,8 +119,8 @@ fi # Step 7: Deploy agent echo "" -echo "Step 7: Deploying agent 'ai_services_agent_v1'..." -if orchestrate agents deploy --name ai_services_agent_v1 2>/dev/null; then +echo "Step 7: Deploying agent 'ai_services_agent_v2'..." +if orchestrate agents deploy --name ai_services_agent_v2 2>/dev/null; then echo "✓ Agent deployed successfully" else echo "⚠ Failed to deploy agent (may already be deployed or configuration error)" diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py index 2343b96..9f825b2 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py @@ -11,8 +11,8 @@ TOOLKIT_PATH = ROOT_DIR / "toolkit.yaml" AGENT_PATH = ROOT_DIR / "agent.yaml" -DEFAULT_TOOLKIT_NAME = "ai-services-toolkit-v1" -DEFAULT_AGENT_NAME = "ai_services_agent_v1" +DEFAULT_TOOLKIT_NAME = "ai-services-toolkit-v2" +DEFAULT_AGENT_NAME = "ai_services_agent_v2" DEFAULT_TOOL_NAME = "get_ai_service_response" DEFAULT_SERVER_NAME = DEFAULT_TOOLKIT_NAME DEFAULT_LLM_NAME = "groq/openai/gpt-oss-120b" From bae1ce81465488436e3325a5bce62ee3a3f5c3e8 Mon Sep 17 00:00:00 2001 From: Karol Zmorski Date: Fri, 3 Jul 2026 18:01:57 +0200 Subject: [PATCH 12/12] V3 for list of deployed AI Services working as a separate tools within a toolkit --- .../mcp_server/server.py | 218 +++++++++++++----- .../mcp_server/utils.py | 157 ++++++------- .../scripts/cleanup.sh | 12 +- .../scripts/deploy.sh | 56 ++++- .../scripts/generate_template.py | 203 +++++++++++----- .../template.env | 9 +- 6 files changed, 443 insertions(+), 212 deletions(-) diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py index 9777e7a..8cd442d 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py @@ -1,34 +1,55 @@ from mcp.server.fastmcp import FastMCP from typing import Any, Dict +from functools import lru_cache +import sys +import logging from utils import ( - get_deployment_name, - get_deployment_description, get_tool_name, - get_ai_service_deployment_id, + get_ai_service_deployment_ids, get_server_name, prepare_api_client, get_request_schema, - get_response_schema, create_pydantic_model_from_schema, build_payload_from_schema, - extract_output_from_response, + get_deployment_details, + load_env, ) +# Configure logging to stderr to avoid breaking JSONRPC protocol +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + stream=sys.stderr, +) +logger = logging.getLogger(__name__) + +# Initialize MCP server mcp = FastMCP(get_server_name()) +# Flag to track if tools have been registered +_tools_registered = False + -def _get_dynamic_input_model(): +@lru_cache(maxsize=128) +def _get_dynamic_input_model(deployment_id: str): """ Dynamically create the input model based on the deployment's request schema. Falls back to a simple model if schema is not available. + + Args: + deployment_id: The deployment ID to create model for """ try: - request_schema = get_request_schema() + request_schema = get_request_schema(deployment_id) if request_schema: - return create_pydantic_model_from_schema(request_schema, "ToolInputSchema") + return create_pydantic_model_from_schema( + request_schema, f"ToolInputSchema_{deployment_id.replace('-', '_')}" + ) except Exception as e: - print(f"Warning: Could not create dynamic model from schema: {e}") + logger.warning( + f"Could not create dynamic model from schema for {deployment_id}: {e}" + ) # Fallback to a generic model from pydantic import BaseModel, Field @@ -39,80 +60,149 @@ class FallbackInputSchema(BaseModel): return FallbackInputSchema -def _build_tool_description() -> str: +def _build_tool_description(deployment_id: str, deployment_index: str) -> str: """ Build tool description dynamically based on deployment details and schema. - """ - deployment_name = get_deployment_name() - deployment_description = get_deployment_description() - description = f"Invoke the '{deployment_name}' AI service.\n\n" + Args: + deployment_id: The deployment ID + deployment_index: The index/identifier for this deployment + """ + api_client = prepare_api_client() - if deployment_description: - description += f"{deployment_description}\n\n" + try: + deployment_details = get_deployment_details(api_client, deployment_id) + deployment_name = deployment_details.get("metadata", {}).get( + "name", f"Deployment {deployment_index}" + ) + deployment_description = deployment_details.get("metadata", {}).get( + "description", "" + ) - # try: - # request_schema = get_request_schema() - # if request_schema and "properties" in request_schema: - # description += "Input parameters:\n" - # for prop_name, prop_schema in request_schema["properties"].items(): - # prop_desc = prop_schema.get("title", "") or prop_schema.get( - # "description", "" - # ) - # prop_type = prop_schema.get("type", "string") - # required = prop_name in request_schema.get("required", []) - # req_marker = " (required)" if required else " (optional)" - # description += f"- {prop_name} ({prop_type}){req_marker}: {prop_desc}\n" - # except Exception: - # pass + description = f"Invoke the '{deployment_name}' AI service (Deployment {deployment_index}).\n\n" - return description + if deployment_description: + description += f"{deployment_description}\n\n" + description += f"Deployment ID: {deployment_id}" -# Create the dynamic input model -ToolInputSchema = _get_dynamic_input_model() + return description + except Exception as e: + logger.warning(f"Could not fetch deployment details for {deployment_id}: {e}") + return f"Invoke AI service deployment {deployment_index}.\n\nDeployment ID: {deployment_id}" -@mcp.tool(name=get_tool_name(), description=_build_tool_description()) -def invoke_tool(input_data: ToolInputSchema) -> Dict[str, Any]: +def _create_tool_function(deployment_id: str, deployment_index: str): """ - Generic tool that invokes an AI service deployment using dynamic schema-based payload construction. + Create a tool function for a specific deployment. + + Args: + deployment_id: The deployment ID + deployment_index: The index/identifier for this deployment """ - api_client = prepare_api_client() - deployment_id = get_ai_service_deployment_id() + # Get the dynamic input model for this deployment + ToolInputSchema = _get_dynamic_input_model(deployment_id) + + def invoke_tool(input_data: ToolInputSchema) -> Dict[str, Any]: + """ + Tool that invokes an AI service deployment using dynamic schema-based payload construction. + """ + api_client = prepare_api_client() + + # Convert Pydantic model to dict + input_dict = input_data.model_dump() + + # Build payload based on request schema + try: + request_schema = get_request_schema(deployment_id) + payload = ( + build_payload_from_schema(input_dict, request_schema) + if request_schema + else input_dict + ) + except Exception as e: + logger.warning(f"Could not build payload from schema, using raw input: {e}") + payload = input_dict + + # Call the AI service + response = api_client.deployments.run_ai_service(deployment_id, payload) + + return response + + return invoke_tool - # Convert Pydantic model to dict - input_dict = input_data.model_dump() - # Build payload based on request schema +def validate_environment(): + """ + Validate that all required environment variables are set. + Raises ValueError if validation fails. + """ + # Load environment variables + load_env() + + # Try to get deployment IDs to validate environment try: - request_schema = get_request_schema() - payload = ( - build_payload_from_schema(input_dict, request_schema) - if request_schema - else input_dict + deployment_ids = get_ai_service_deployment_ids() + logger.info( + f"Environment validation successful: Found {len(deployment_ids)} deployment ID(s)" ) - except Exception as e: - print(f"Warning: Could not build payload from schema, using raw input: {e}") - payload = input_dict + return deployment_ids + except ValueError as e: + logger.error(f"Environment validation failed: {e}") + raise + + +def register_tools(): + """ + Register a separate tool for each deployment ID found in environment variables. + This function is called lazily to ensure environment is loaded first. + """ + global _tools_registered + + if _tools_registered: + logger.info("Tools already registered, skipping") + return + + try: + # Validate environment and get deployment IDs + deployment_ids = validate_environment() + + logger.info(f"Registering tools for {len(deployment_ids)} deployment(s)") + + for index, deployment_id in deployment_ids.items(): + # Create tool name based on index + if index == "default": + tool_name = get_tool_name() + else: + tool_name = f"{get_tool_name()}_{index}" + + # Build description + description = _build_tool_description(deployment_id, index) + + # Create and register the tool + tool_func = _create_tool_function(deployment_id, index) - # Call the AI service - response = api_client.deployments.run_ai_service(deployment_id, payload) + # Register with MCP + mcp.tool(name=tool_name, description=description)(tool_func) - # Extract output based on response schema - # try: - # response_schema = get_response_schema() - # output = ( - # extract_output_from_response(response, response_schema) - # if response_schema - # else response - # ) - # except Exception as e: - # print(f"Warning: Could not extract output from schema, using raw response: {e}") - # output = response + logger.info(f"Registered tool: {tool_name} for deployment {deployment_id}") - return response + _tools_registered = True + logger.info("Tool registration complete") + + except Exception as e: + logger.error(f"Error registering tools: {e}", exc_info=True) + raise if __name__ == "__main__": - mcp.run(transport="stdio") + try: + # Register tools before starting the server + register_tools() + + # Start the MCP server + logger.info("Starting MCP server") + mcp.run(transport="stdio") + except Exception as e: + logger.error(f"Failed to start server: {e}", exc_info=True) + sys.exit(1) diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py index 2bac855..82bfedb 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py @@ -7,8 +7,8 @@ from pydantic import BaseModel, Field, create_model from generated_config import ( - DEPLOYMENT_NAME, - DEPLOYMENT_DESCRIPTION, + # DEPLOYMENT_NAME, + # DEPLOYMENT_DESCRIPTION, SERVER_NAME, TOOL_NAME, ) @@ -30,6 +30,9 @@ def prepare_api_client() -> APIClient: def get_ai_service_deployment_id() -> str: + """ + Get single deployment ID (legacy support). + """ load_env() deployment_id = os.getenv("WATSONX_AI_SERVICE_DEPLOYMENT_ID") if not deployment_id: @@ -37,6 +40,43 @@ def get_ai_service_deployment_id() -> str: return deployment_id +def get_ai_service_deployment_ids() -> Dict[str, str]: + """ + Get all deployment IDs from environment variables. + Supports both legacy single ID and new multiple ID format. + + Returns: + Dictionary mapping deployment index/name to deployment ID + Example: {"1": "id1", "2": "id2"} or {"default": "single_id"} + """ + load_env() + deployment_ids = {} + + # Check for legacy single deployment ID + single_id = os.getenv("WATSONX_AI_SERVICE_DEPLOYMENT_ID") + if single_id: + deployment_ids["default"] = single_id + return deployment_ids + + # Check for numbered deployment IDs + index = 1 + while True: + env_var = f"WATSONX_AI_SERVICE_DEPLOYMENT_ID_{index}" + deployment_id = os.getenv(env_var) + if not deployment_id: + break + deployment_ids[str(index)] = deployment_id + index += 1 + + if not deployment_ids: + raise ValueError( + "No deployment IDs found. Set either WATSONX_AI_SERVICE_DEPLOYMENT_ID " + "or WATSONX_AI_SERVICE_DEPLOYMENT_ID_1, WATSONX_AI_SERVICE_DEPLOYMENT_ID_2, etc." + ) + + return deployment_ids + + def get_server_name() -> str: return SERVER_NAME @@ -45,12 +85,12 @@ def get_tool_name() -> str: return TOOL_NAME -def get_deployment_name() -> str: - return DEPLOYMENT_NAME - - -def get_deployment_description() -> str: - return DEPLOYMENT_DESCRIPTION +# def get_deployment_name() -> str: +# return DEPLOYMENT_NAME +# +# +# def get_deployment_description() -> str: +# return DEPLOYMENT_DESCRIPTION def get_required_env_status() -> dict: @@ -59,66 +99,70 @@ def get_required_env_status() -> dict: "WATSONX_URL", "WATSONX_API_KEY", "WATSONX_SPACE_ID", - "WATSONX_AI_SERVICE_DEPLOYMENT_ID", ] - return {env_var: bool(os.getenv(env_var)) for env_var in required_env_vars} + status = {env_var: bool(os.getenv(env_var)) for env_var in required_env_vars} + # Check for at least one deployment ID + has_deployment_id = bool(os.getenv("WATSONX_AI_SERVICE_DEPLOYMENT_ID")) + if not has_deployment_id: + # Check for numbered IDs + has_deployment_id = bool(os.getenv("WATSONX_AI_SERVICE_DEPLOYMENT_ID_1")) -def get_deployment_details(api_client: APIClient) -> Dict[str, Any]: - """ - Fetch deployment details. + status["WATSONX_AI_SERVICE_DEPLOYMENT_ID"] = has_deployment_id + + return status + + +def get_deployment_details(api_client: APIClient, deployment_id: str) -> Dict[str, Any]: """ - deployment_id = get_ai_service_deployment_id() + Fetch deployment details for a specific deployment ID. + Args: + api_client: Watson API client + deployment_id: The deployment ID to fetch details for + """ try: details = api_client.deployments.get_details(deployment_id) return details except Exception as e: - raise RuntimeError(f"Failed to fetch deployment details: {e}") + raise RuntimeError( + f"Failed to fetch deployment details for {deployment_id}: {e}" + ) -@lru_cache(maxsize=1) -def get_ai_service_details() -> Dict[str, Any]: +@lru_cache(maxsize=128) +def get_ai_service_details(deployment_id: str) -> Dict[str, Any]: """ Fetch AI service details including documentation schema. Cached to avoid repeated API calls. + + Args: + deployment_id: The deployment ID to fetch AI service details for """ api_client = prepare_api_client() - deployment_details = get_deployment_details(api_client) + deployment_details = get_deployment_details(api_client, deployment_id) ai_service_id = deployment_details["entity"]["asset"]["id"] return api_client.repository.get_ai_service_details(ai_service_id) -def get_request_schema() -> Optional[Dict[str, Any]]: +def get_request_schema(deployment_id: str) -> Optional[Dict[str, Any]]: """ Extract request schema from deployment documentation. Returns the JSON schema for the request payload. + + Args: + deployment_id: The deployment ID to get schema for """ - details = get_ai_service_details() + details = get_ai_service_details(deployment_id) try: documentation = details.get("entity", {}).get("documentation", {}) request_schema = documentation.get("request", {}).get("application/json", {}) return request_schema if request_schema else None except (KeyError, AttributeError): - return None - - -def get_response_schema() -> Optional[Dict[str, Any]]: - """ - Extract response schema from deployment documentation. - Returns the JSON schema for the response payload. - """ - details = get_ai_service_details() - - try: - documentation = details.get("entity", {}).get("documentation", {}) - response_schema = documentation.get("response", {}).get("application/json", {}) - return response_schema if response_schema else None - except (KeyError, AttributeError): - return None + raise RuntimeError(f"Given AI Service with deployment id `{deployment_id}` does not include request schema.") def build_payload_from_schema( @@ -147,47 +191,6 @@ def build_payload_from_schema( return payload -def extract_output_from_response( - response: Dict[str, Any], schema: Dict[str, Any] -) -> Any: - """ - Extract relevant output from API response based on the response schema. - - Args: - response: Raw API response - schema: JSON schema defining the response structure - - Returns: - Extracted output value(s) - """ - if not schema or "properties" not in schema: - return response - - properties = schema.get("properties", {}) - - # Try to extract the main content based on common patterns - # For chat-like responses, look for choices[0].message.content - if "choices" in properties and "choices" in response: - choices = response.get("choices", []) - if choices and len(choices) > 0: - message = choices[0].get("message", {}) - if "content" in message: - return message["content"] - # Handle streaming delta format - if "delta" in message: - delta = message.get("delta", {}) - if "content" in delta: - return delta["content"] - - # For simpler responses, return the first property value - for prop_name in properties.keys(): - if prop_name in response: - return response[prop_name] - - # Fallback to returning the entire response - return response - - def create_pydantic_model_from_schema( schema: Dict[str, Any], model_name: str = "DynamicModel" ) -> Type[BaseModel]: diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh index 51f767c..6847816 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh @@ -9,8 +9,8 @@ echo "=========================================================" # Step 1: Undeploy the agent echo "" -echo "Step 1: Undeploying agent 'ai_services_agent_v2'..." -if orchestrate agents undeploy --name ai_services_agent_v2 2>/dev/null; then +echo "Step 1: Undeploying agent 'ai_services_agent_v3'..." +if orchestrate agents undeploy --name ai_services_agent_v3 2>/dev/null; then echo "✓ Agent undeployed successfully" else echo "⚠ Failed to undeploy agent (may not exist or already undeployed)" @@ -18,8 +18,8 @@ fi # Step 2: Remove the agent echo "" -echo "Step 2: Removing agent 'ai_services_agent_v2'..." -if orchestrate agents remove --name ai_services_agent_v2 --kind native 2>/dev/null; then +echo "Step 2: Removing agent 'ai_services_agent_v3'..." +if orchestrate agents remove --name ai_services_agent_v3 --kind native 2>/dev/null; then echo "✓ Agent removed successfully" else echo "⚠ Failed to remove agent (may not exist)" @@ -27,8 +27,8 @@ fi # Step 3: Remove the toolkit echo "" -echo "Step 3: Removing toolkit 'ai-services-toolkit-v2'..." -if orchestrate toolkits remove --name ai-services-toolkit-v2 2>/dev/null; then +echo "Step 3: Removing toolkit 'ai-services-toolkit-v3'..." +if orchestrate toolkits remove --name ai-services-toolkit-v3 2>/dev/null; then echo "✓ Toolkit removed successfully" else echo "⚠ Failed to remove toolkit (may not exist)" diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh index 8fa55bf..fe15ab7 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh @@ -49,7 +49,27 @@ if export $(grep -v '^#' .env | xargs) 2>/dev/null; then echo " - WATSONX_URL: ${WATSONX_URL:0:30}..." echo " - WATSONX_API_KEY: ${WATSONX_API_KEY:0:10}..." echo " - WATSONX_SPACE_ID: $WATSONX_SPACE_ID" - echo " - WATSONX_AI_SERVICE_DEPLOYMENT_ID: $WATSONX_AI_SERVICE_DEPLOYMENT_ID" + + # Display deployment IDs (supports both single and multiple) + if [ -n "$WATSONX_AI_SERVICE_DEPLOYMENT_ID" ]; then + echo " - WATSONX_AI_SERVICE_DEPLOYMENT_ID: $WATSONX_AI_SERVICE_DEPLOYMENT_ID" + else + # Check for numbered deployment IDs + deployment_count=0 + for i in {1..10}; do + var_name="WATSONX_AI_SERVICE_DEPLOYMENT_ID_$i" + if [ -n "${!var_name}" ]; then + echo " - WATSONX_AI_SERVICE_DEPLOYMENT_ID_$i: ${!var_name}" + deployment_count=$((deployment_count + 1)) + else + break + fi + done + + if [ $deployment_count -eq 0 ]; then + echo " ⚠ Warning: No deployment IDs found" + fi + fi else echo "❌ Failed to load environment variables from .env file" exit 1 @@ -75,14 +95,28 @@ for env in draft live; do continue fi - # Set credentials - if orchestrate connections set-credentials \ - -a ai-service-connection \ - --env $env \ - -e "WATSONX_URL=$WATSONX_URL" \ - -e "WATSONX_API_KEY=$WATSONX_API_KEY" \ - -e "WATSONX_SPACE_ID=$WATSONX_SPACE_ID" \ - -e "WATSONX_AI_SERVICE_DEPLOYMENT_ID=$WATSONX_AI_SERVICE_DEPLOYMENT_ID" 2>/dev/null; then + # Set credentials - build the command dynamically to support multiple deployment IDs + credentials_cmd="orchestrate connections set-credentials -a ai-service-connection --env $env" + credentials_cmd="$credentials_cmd -e \"WATSONX_URL=$WATSONX_URL\"" + credentials_cmd="$credentials_cmd -e \"WATSONX_API_KEY=$WATSONX_API_KEY\"" + credentials_cmd="$credentials_cmd -e \"WATSONX_SPACE_ID=$WATSONX_SPACE_ID\"" + + # Add single deployment ID if it exists + if [ -n "$WATSONX_AI_SERVICE_DEPLOYMENT_ID" ]; then + credentials_cmd="$credentials_cmd -e \"WATSONX_AI_SERVICE_DEPLOYMENT_ID=$WATSONX_AI_SERVICE_DEPLOYMENT_ID\"" + fi + + # Add numbered deployment IDs + for i in {1..10}; do + var_name="WATSONX_AI_SERVICE_DEPLOYMENT_ID_$i" + if [ -n "${!var_name}" ]; then + credentials_cmd="$credentials_cmd -e \"WATSONX_AI_SERVICE_DEPLOYMENT_ID_$i=${!var_name}\"" + else + break + fi + done + + if eval "$credentials_cmd" 2>/dev/null; then echo " ✓ Credentials set for $env environment" else echo " ⚠ Failed to set credentials for $env environment" @@ -119,8 +153,8 @@ fi # Step 7: Deploy agent echo "" -echo "Step 7: Deploying agent 'ai_services_agent_v2'..." -if orchestrate agents deploy --name ai_services_agent_v2 2>/dev/null; then +echo "Step 7: Deploying agent 'ai_services_agent_v3'..." +if orchestrate agents deploy --name ai_services_agent_v3 2>/dev/null; then echo "✓ Agent deployed successfully" else echo "⚠ Failed to deploy agent (may already be deployed or configuration error)" diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py index 9f825b2..e1fe0b8 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py @@ -11,8 +11,8 @@ TOOLKIT_PATH = ROOT_DIR / "toolkit.yaml" AGENT_PATH = ROOT_DIR / "agent.yaml" -DEFAULT_TOOLKIT_NAME = "ai-services-toolkit-v2" -DEFAULT_AGENT_NAME = "ai_services_agent_v2" +DEFAULT_TOOLKIT_NAME = "ai-services-toolkit-v3" +DEFAULT_AGENT_NAME = "ai_services_agent_v3" DEFAULT_TOOL_NAME = "get_ai_service_response" DEFAULT_SERVER_NAME = DEFAULT_TOOLKIT_NAME DEFAULT_LLM_NAME = "groq/openai/gpt-oss-120b" @@ -123,64 +123,98 @@ def write_generated_config(metadata: dict[str, Any]) -> None: def build_toolkit_yaml() -> dict[str, Any]: + """ + Build toolkit YAML configuration. + Supports both legacy single deployment ID and new multiple deployment IDs. + """ + import os + + # Determine which env vars to include + env_vars = [ + "WATSONX_URL", + "WATSONX_API_KEY", + "WATSONX_SPACE_ID", + ] + + # Check if using legacy single ID or new multiple IDs + if os.getenv("WATSONX_AI_SERVICE_DEPLOYMENT_ID"): + env_vars.append("WATSONX_AI_SERVICE_DEPLOYMENT_ID") + else: + # Add numbered deployment IDs + index = 1 + while os.getenv(f"WATSONX_AI_SERVICE_DEPLOYMENT_ID_{index}"): + env_vars.append(f"WATSONX_AI_SERVICE_DEPLOYMENT_ID_{index}") + index += 1 + return { "spec_version": "v1", "kind": "mcp", "name": DEFAULT_TOOLKIT_NAME, - "description": "Generic watsonx.ai AI Services toolkit with deployed AI service as a tool", + "description": "Generic watsonx.ai AI Services toolkit with deployed AI service(s) as tool(s)", "command": "python server.py", - "env": [ - "WATSONX_URL", - "WATSONX_API_KEY", - "WATSONX_SPACE_ID", - "WATSONX_AI_SERVICE_DEPLOYMENT_ID", - ], + "env": env_vars, "tools": ["*"], "package_root": "./mcp_server", } def build_agent_yaml( - deployment_name: str, - deployment_description: str, - ai_service_name: str, - ai_service_description: str, + deployments_info: list[dict[str, str]], ) -> dict[str, Any]: + """ + Build agent YAML configuration. + + Args: + deployments_info: List of dicts containing deployment information + Each dict has: deployment_name, deployment_description, + ai_service_name, ai_service_description, tool_name + """ + # Build the available services section + services_list = [] + for idx, info in enumerate(deployments_info, 1): + services_list.append( + f"{idx}. {info['deployment_name']}\n" + f" - Description: {info['deployment_description']}\n" + f" - AI Service: {info['ai_service_name']}\n" + f" - Tool: {info['tool_name']}" + ) + + services_text = "\n".join(services_list) + instructions = ( "You are an assistant powered by deployed watsonx.ai AI services.\n\n" - "AVAILABLE AI SERVICE:\n" - f"- Deployment Name: {deployment_name}\n" - f"- Deployment Description: {deployment_description}\n" - f"- AI Service Name: {ai_service_name}\n" - f"- AI Service Description: {ai_service_description}\n\n" + "AVAILABLE AI SERVICES:\n" + f"{services_text}\n\n" "STRICT RULES — follow these without exception:\n" "1. When the user says hello or asks what you can do, list the available tools and briefly explain their purpose and required inputs.\n" - "2. ONLY call the AI service tool when the user's request is clearly related to the deployment or AI service based on their names and descriptions above.\n" + "2. ONLY call an AI service tool when the user's request is clearly related to that specific deployment or AI service based on their names and descriptions above.\n" "3. If one or more required tool input fields are missing, ask only for the missing fields.\n" "4. Analyze the user's request carefully:\n" - " - If it matches the purpose described in the deployment/service descriptions, use the tool\n" - " - If it's a general question unrelated to the AI service's purpose, answer from your own knowledge\n" - " - When in doubt about relevance, ask the user for clarification rather than calling the tool\n" + " - If it matches the purpose described in a deployment/service description, use that tool\n" + " - If it's a general question unrelated to any AI service's purpose, answer from your own knowledge\n" + " - When in doubt about relevance, ask the user for clarification rather than calling a tool\n" "5. If a tool returns an error, explain the failure clearly and do not invent missing information.\n" - "6. When the tool returns a result, retrieve only helpful data based on a user's request and provide them to the user.\n" - "7. Be helpful and conversational, but precise about when to use the AI service tool.\n\n" - "Your role is to intelligently route requests to the AI service only when appropriate, based on the service's documented purpose." + "6. When a tool returns a result, retrieve only helpful data based on the user's request and provide them to the user.\n" + "7. Be helpful and conversational, but precise about when to use each AI service tool.\n" + "8. If multiple services could handle a request, choose the most appropriate one or ask the user which they prefer.\n\n" + "Your role is to intelligently route requests to the appropriate AI service only when relevant, based on each service's documented purpose." ) + # Build tools list + tools = [f"{DEFAULT_TOOLKIT_NAME}:{info['tool_name']}" for info in deployments_info] + return { "spec_version": "v1", "kind": "native", "name": DEFAULT_AGENT_NAME, "description": ( - "Answers user questions using a watsonx.ai deployed AI service when appropriate." + "Answers user questions using watsonx.ai deployed AI services when appropriate." ), "llm": DEFAULT_LLM_NAME, "style": "react", "hide_reasoning": False, "instructions": instructions, - "tools": [ - f"{DEFAULT_TOOLKIT_NAME}:{DEFAULT_TOOL_NAME}", - ], + "tools": tools, "collaborators": [], } @@ -203,45 +237,108 @@ def write_yaml(path: Path, content: dict[str, Any]) -> None: ) +def get_all_deployment_ids() -> dict[str, str]: + """ + Get all deployment IDs from environment variables. + Supports both legacy single ID and new multiple ID format. + + Returns: + Dictionary mapping deployment index/name to deployment ID + """ + import os + + deployment_ids = {} + + # Check for legacy single deployment ID + single_id = os.getenv("WATSONX_AI_SERVICE_DEPLOYMENT_ID") + if single_id: + deployment_ids["default"] = single_id + return deployment_ids + + # Check for numbered deployment IDs + index = 1 + while True: + env_var = f"WATSONX_AI_SERVICE_DEPLOYMENT_ID_{index}" + deployment_id = os.getenv(env_var) + if not deployment_id: + break + deployment_ids[str(index)] = deployment_id + index += 1 + + if not deployment_ids: + raise ValueError( + "No deployment IDs found. Set either WATSONX_AI_SERVICE_DEPLOYMENT_ID " + "or WATSONX_AI_SERVICE_DEPLOYMENT_ID_1, WATSONX_AI_SERVICE_DEPLOYMENT_ID_2, etc." + ) + + return deployment_ids + + def main() -> None: load_env() client = prepare_api_client() - deployment_id = require_env("WATSONX_AI_SERVICE_DEPLOYMENT_ID") + # Get all deployment IDs + deployment_ids = get_all_deployment_ids() - deployment_details = get_deployment_details(client, deployment_id) - deployment_name = get_deployment_name(deployment_details) - deployment_description = get_deployment_description(deployment_details) + print(f"Found {len(deployment_ids)} deployment ID(s)") - ai_service_id = get_ai_service_id(deployment_details) - ai_service_details = get_ai_service_details(client, ai_service_id) - ai_service_name = get_ai_service_name(ai_service_details) - ai_service_description = get_ai_service_description(ai_service_details) + # Collect information for all deployments + deployments_info = [] + all_metadata = [] - metadata = build_metadata( - deployment_id=deployment_id, - deployment_name=deployment_name, - deployment_description=deployment_description, - ai_service_id=ai_service_id, - ai_service_name=ai_service_name, - ai_service_description=ai_service_description, - ) - write_generated_config(metadata) + for index, deployment_id in deployment_ids.items(): + print(f"\nProcessing deployment {index}: {deployment_id}") + + deployment_details = get_deployment_details(client, deployment_id) + deployment_name = get_deployment_name(deployment_details) + deployment_description = get_deployment_description(deployment_details) + + ai_service_id = get_ai_service_id(deployment_details) + ai_service_details = get_ai_service_details(client, ai_service_id) + ai_service_name = get_ai_service_name(ai_service_details) + ai_service_description = get_ai_service_description(ai_service_details) + + # Determine tool name + if index == "default": + tool_name = DEFAULT_TOOL_NAME + else: + tool_name = f"{DEFAULT_TOOL_NAME}_{index}" + + metadata = build_metadata( + deployment_id=deployment_id, + deployment_name=deployment_name, + deployment_description=deployment_description, + ai_service_id=ai_service_id, + ai_service_name=ai_service_name, + ai_service_description=ai_service_description, + ) + all_metadata.append(metadata) + + deployments_info.append( + { + "deployment_name": deployment_name, + "deployment_description": deployment_description, + "ai_service_name": ai_service_name, + "ai_service_description": ai_service_description, + "tool_name": tool_name, + } + ) + + # Write generated config for the first deployment (for backward compatibility) + # In a multi-deployment scenario, this serves as a reference + write_generated_config(all_metadata[0]) toolkit_yaml = build_toolkit_yaml() - agent_yaml = build_agent_yaml( - deployment_name=deployment_name, - deployment_description=deployment_description, - ai_service_name=ai_service_name, - ai_service_description=ai_service_description, - ) + agent_yaml = build_agent_yaml(deployments_info) write_yaml(TOOLKIT_PATH, toolkit_yaml) write_yaml(AGENT_PATH, agent_yaml) - print(f"Generated {TOOLKIT_PATH}") + print(f"\nGenerated {TOOLKIT_PATH}") print(f"Generated {AGENT_PATH}") print(f"Generated {GENERATED_CONFIG_PATH}") + print(f"\nTotal deployments configured: {len(deployment_ids)}") if __name__ == "__main__": diff --git a/agents/community/mcp-orchestrate-ai-service-template-generic/template.env b/agents/community/mcp-orchestrate-ai-service-template-generic/template.env index 4316c99..f5fb626 100644 --- a/agents/community/mcp-orchestrate-ai-service-template-generic/template.env +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/template.env @@ -2,4 +2,11 @@ WATSONX_URL=https://us-south.ml.cloud.ibm.com WATSONX_API_KEY=your_api_key WATSONX_SPACE_ID=your_space_id -WATSONX_AI_SERVICE_DEPLOYMENT_ID=your_autoai_rag_pattern_deployment_id \ No newline at end of file +# Single deployment ID (legacy support) +# WATSONX_AI_SERVICE_DEPLOYMENT_ID=your_autoai_rag_pattern_deployment_id + +# Multiple deployment IDs (recommended) +# Add as many as needed, incrementing the number +WATSONX_AI_SERVICE_DEPLOYMENT_ID_1=your_first_deployment_id +WATSONX_AI_SERVICE_DEPLOYMENT_ID_2=your_second_deployment_id +# WATSONX_AI_SERVICE_DEPLOYMENT_ID_3=your_third_deployment_id \ No newline at end of file