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..8cd442d --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/server.py @@ -0,0 +1,208 @@ +from mcp.server.fastmcp import FastMCP +from typing import Any, Dict +from functools import lru_cache +import sys +import logging + +from utils import ( + get_tool_name, + get_ai_service_deployment_ids, + get_server_name, + prepare_api_client, + get_request_schema, + create_pydantic_model_from_schema, + build_payload_from_schema, + 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 + + +@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(deployment_id) + if request_schema: + return create_pydantic_model_from_schema( + request_schema, f"ToolInputSchema_{deployment_id.replace('-', '_')}" + ) + except Exception as 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 + + class FallbackInputSchema(BaseModel): + input: str = Field(..., description="Input data for the AI service") + + return FallbackInputSchema + + +def _build_tool_description(deployment_id: str, deployment_index: str) -> str: + """ + Build tool description dynamically based on deployment details and schema. + + Args: + deployment_id: The deployment ID + deployment_index: The index/identifier for this deployment + """ + api_client = prepare_api_client() + + 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", "" + ) + + description = f"Invoke the '{deployment_name}' AI service (Deployment {deployment_index}).\n\n" + + if deployment_description: + description += f"{deployment_description}\n\n" + + description += f"Deployment ID: {deployment_id}" + + 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}" + + +def _create_tool_function(deployment_id: str, deployment_index: str): + """ + Create a tool function for a specific deployment. + + Args: + deployment_id: The deployment ID + deployment_index: The index/identifier for this deployment + """ + # 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 + + +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: + deployment_ids = get_ai_service_deployment_ids() + logger.info( + f"Environment validation successful: Found {len(deployment_ids)} deployment ID(s)" + ) + 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) + + # Register with MCP + mcp.tool(name=tool_name, description=description)(tool_func) + + logger.info(f"Registered tool: {tool_name} for deployment {deployment_id}") + + _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__": + 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 new file mode 100644 index 0000000..82bfedb --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/mcp_server/utils.py @@ -0,0 +1,271 @@ +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: + """ + Get single deployment ID (legacy support). + """ + 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_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 + + +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", + ] + + 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")) + + status["WATSONX_AI_SERVICE_DEPLOYMENT_ID"] = has_deployment_id + + return status + + +def get_deployment_details(api_client: APIClient, deployment_id: str) -> Dict[str, Any]: + """ + 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 for {deployment_id}: {e}" + ) + + +@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_id) + ai_service_id = deployment_details["entity"]["asset"]["id"] + + return api_client.repository.get_ai_service_details(ai_service_id) + + +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(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): + raise RuntimeError(f"Given AI Service with deployment id `{deployment_id}` does not include request schema.") + + +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 create_pydantic_model_from_schema( + schema: Dict[str, Any], model_name: str = "DynamicModel" +) -> Type[BaseModel]: + """ + Create a Pydantic model dynamically from a JSON schema. + Recursively handles nested objects and arrays. + + 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(): + # 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", "" + ) + 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], 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 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, 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, # Fallback for objects without properties + "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..6847816 --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/cleanup.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# 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 AI Service orchestration resources..." +echo "=========================================================" + +# Step 1: Undeploy the agent +echo "" +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)" +fi + +# Step 2: Remove the agent +echo "" +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)" +fi + +# Step 3: Remove the toolkit +echo "" +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)" +fi + +# Step 4: Remove the connection +echo "" +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)" +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..fe15ab7 --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/deploy.sh @@ -0,0 +1,171 @@ +#!/bin/bash + +# 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 AI Service 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 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" + 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_AI_SERVICE_DEPLOYMENT_ID is correct" + exit 1 +fi + +# Step 2: Add connection +echo "" +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)" +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" + + # 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 +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 ai-service-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 - 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" + 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 ai-service-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 '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)" +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..e1fe0b8 --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/scripts/generate_template.py @@ -0,0 +1,345 @@ +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-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" + + +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]: + """ + 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(s) as tool(s)", + "command": "python server.py", + "env": env_vars, + "tools": ["*"], + "package_root": "./mcp_server", + } + + +def build_agent_yaml( + 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 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 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 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 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 watsonx.ai deployed AI services when appropriate." + ), + "llm": DEFAULT_LLM_NAME, + "style": "react", + "hide_reasoning": False, + "instructions": instructions, + "tools": tools, + "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 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() + + # Get all deployment IDs + deployment_ids = get_all_deployment_ids() + + print(f"Found {len(deployment_ids)} deployment ID(s)") + + # Collect information for all deployments + deployments_info = [] + all_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(deployments_info) + + write_yaml(TOOLKIT_PATH, toolkit_yaml) + write_yaml(AGENT_PATH, agent_yaml) + + 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__": + 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..f5fb626 --- /dev/null +++ b/agents/community/mcp-orchestrate-ai-service-template-generic/template.env @@ -0,0 +1,12 @@ +WATSONX_URL=https://us-south.ml.cloud.ibm.com +WATSONX_API_KEY=your_api_key +WATSONX_SPACE_ID=your_space_id + +# 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 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..94d823b --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/server.py @@ -0,0 +1,71 @@ +from mcp.server.fastmcp import FastMCP +from pydantic import BaseModel, Field + +from utils import ( + get_deployment_name, + get_deployment_description, + get_rag_answer_tool_name, + get_rag_deployment_id, + get_rag_deployment_details_tool_name, + get_required_env_status, + get_server_name, + prepare_api_client, +) + +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() + + 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_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() + + 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": 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_description": get_deployment_description(), + "deployment_id": deployment_id, + "server_name": get_server_name(), + "required_environment_variables": get_required_env_status(), + } + + +if __name__ == "__main__": + mcp.run(transport="stdio") 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..89b9b37 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/mcp_server/utils.py @@ -0,0 +1,66 @@ +import os + +from dotenv import load_dotenv +from ibm_watsonx_ai import APIClient, Credentials + +from generated_config import ( + DEPLOYMENT_NAME, + DEPLOYMENT_DESCRIPTION, + RAG_ANSWER_TOOL_NAME, + RAG_DEPLOYMENT_DETAILS_TOOL_NAME, + SERVER_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_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 + +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_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 new file mode 100644 index 0000000..45cc8dc --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-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-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..0daea39 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-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-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..654a4bc --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-rag-pattern-template-generic/scripts/generate_template.py @@ -0,0 +1,196 @@ +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-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-v3" +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 build_metadata( + 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, + "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'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) + + +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 with grounded answer and deployment diagnostics tools", + "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, 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. 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" + "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 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_RAG_ANSWER_TOOL_NAME}", + f"{DEFAULT_TOOLKIT_NAME}:{DEFAULT_RAG_DEPLOYMENT_DETAILS_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) + + 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, 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-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 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/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..c3a4126 --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/deploy.sh @@ -0,0 +1,139 @@ +#!/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: Generate template files (toolkit.yaml, agent.yaml, generated_config.py) +echo "" +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 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_DEPLOYMENT_ID: $WATSONX_AUTOAI_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-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 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-prediction-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_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 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..e429a0d --- /dev/null +++ b/agents/community/mcp-orchestrate-autoai-template-generic/scripts/generate_template.py @@ -0,0 +1,205 @@ +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'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