diff --git a/ai/palette-mcp/integrate-palette-mcp/agents/tagging_agent.py b/ai/palette-mcp/integrate-palette-mcp/agents/tagging_agent.py index b2c60e5..3f94fe5 100644 --- a/ai/palette-mcp/integrate-palette-mcp/agents/tagging_agent.py +++ b/ai/palette-mcp/integrate-palette-mcp/agents/tagging_agent.py @@ -11,7 +11,7 @@ from pydantic import BaseModel -from tools import tag_cluster_for_review, tag_cluster_profile_for_review +from helpers import suppress_console_output TAGGING_SYSTEM_PROMPT = ( "You are a cluster tagging specialist. " @@ -52,7 +52,7 @@ class TaggingOutput(BaseModel): notes: str -# async def initialize_tagging_agent(model: str) -> Any: +# async def initialize_tagging_agent(model: str, mcp_tools: list) -> Any: # from langchain.agents import create_agent # from langchain_openai import ChatOpenAI @@ -62,7 +62,7 @@ class TaggingOutput(BaseModel): # llm = ChatOpenAI(model=model) # return create_agent( # model=llm, -# tools=[tag_cluster_for_review, tag_cluster_profile_for_review], +# tools=mcp_tools, # system_prompt=TAGGING_SYSTEM_PROMPT, # response_format=TaggingOutput, # checkpointer=InMemorySaver(), @@ -75,6 +75,7 @@ class TaggingOutput(BaseModel): # profile_discovery_output: str, # active_cluster_output: str, # tags: list[str], +# debug_level: str, # run_id: str, # ) -> str: # if not tags: @@ -99,20 +100,22 @@ class TaggingOutput(BaseModel): # "Task:\n" # "1) Extract unique cluster UIDs from active_clusters_using_matched_profiles.\n" # "2) Extract unique cluster profile UIDs and scope values from matched_profiles.\n" -# f"3) For each cluster UID, call tag_cluster_for_review with cluster_uid= and tags={tags}.\n" -# f"4) For each cluster profile UID, call tag_cluster_profile_for_review with cluster_profile_uid= and tags={tags}, only if scope is not 'system'.\n" +# f"3) For each cluster UID, call manage_resource_tags with action='create', resource_type='spectroclusters', uid=, tags={tags}.\n" +# f"4) For each cluster profile UID, call manage_resource_tags with action='create', resource_type='clusterprofiles', uid=, tags={tags}, only if scope is not 'system'.\n" # "5) For scope='system' profiles, skip tagging and record skip reason.\n" # "6) Return a response that conforms to this JSON schema:\n" # f"{schema}\n" # "If there are no resources to tag, return empty arrays and explain in notes." # ) +# hide_mcp_output = debug_level != "verbose" # run_config = { # "configurable": {"thread_id": f"tagging:{pack_name.lower()}:{run_id}"} # } -# result = await agent.ainvoke( -# {"messages": [{"role": "user", "content": tagging_prompt}]}, -# config=run_config, -# ) +# with suppress_console_output(hide_mcp_output): +# result = await agent.ainvoke( +# {"messages": [{"role": "user", "content": tagging_prompt}]}, +# config=run_config, +# ) # structured = result.get("structured_response") # if isinstance(structured, TaggingOutput): # return structured.model_dump_json() diff --git a/ai/palette-mcp/integrate-palette-mcp/main.py b/ai/palette-mcp/integrate-palette-mcp/main.py index e9801a4..f902ea7 100644 --- a/ai/palette-mcp/integrate-palette-mcp/main.py +++ b/ai/palette-mcp/integrate-palette-mcp/main.py @@ -131,7 +131,7 @@ def parse_args() -> argparse.Namespace: # model=args.active_cluster_model, # mcp_tools=mcp_tools, # ) -# tagging_agent = await initialize_tagging_agent(model=args.tagging_model) +# tagging_agent = await initialize_tagging_agent(model=args.tagging_model, mcp_tools=mcp_tools) # reporter_agent = await initialize_reporter_agent(model=args.reporter_model) # if is_debug_enabled(debug_level, "debug"): @@ -175,6 +175,7 @@ def parse_args() -> argparse.Namespace: # profile_discovery_output, # active_cluster_output, # user_tags, +# debug_level, # run_id, # ) # ) diff --git a/ai/palette-mcp/integrate-palette-mcp/tools.py b/ai/palette-mcp/integrate-palette-mcp/tools.py deleted file mode 100644 index 3daca63..0000000 --- a/ai/palette-mcp/integrate-palette-mcp/tools.py +++ /dev/null @@ -1,210 +0,0 @@ -# Copyright (c) Spectro Cloud -# SPDX-License-Identifier: Apache-2.0 - -"""Local LangChain tools used by the tutorial agent.""" - -from __future__ import annotations - -import os -import json -from typing import Any -import urllib.error -import urllib.request - -from langchain.tools import tool - - -def _read_env_file(path: str) -> dict[str, str]: - values: dict[str, str] = {} - if not os.path.isfile(path): - return values - - with open(path, encoding="utf-8") as env_file: - for raw_line in env_file: - line = raw_line.strip() - if not line or line.startswith("#"): - continue - if line.startswith("export "): - line = line[len("export ") :].strip() - if "=" not in line: - continue - key, value = line.split("=", 1) - key = key.strip() - value = value.strip().strip('"').strip("'") - if key: - values[key] = value - - return values - - -def _resolve_palette_env_file_path() -> str: - return os.path.expanduser("~/.palette/.env-mcp") - - -def _normalize_key(key: str) -> str: - normalized = key.strip().upper() - return "".join(ch for ch in normalized if ch.isalnum()) - - -def _first_non_empty(source: dict[str, str], keys: list[str]) -> str: - normalized_source = {_normalize_key(k): v for k, v in source.items()} - for key in keys: - value = normalized_source.get(_normalize_key(key), "") - if value.strip(): - return value.strip() - return "" - - -def _resolve_palette_credentials() -> tuple[str, str, str]: - project_uid_keys = [ - "SPECTROCLOUD_DEFAULT_PROJECT_ID", - ] - api_key_keys = [ - "SPECTROCLOUD_APIKEY", - ] - host_keys = [ - "SPECTROCLOUD_HOST", - ] - - project_uid = _first_non_empty(os.environ, project_uid_keys) - api_key = _first_non_empty(os.environ, api_key_keys) - host = _first_non_empty(os.environ, host_keys) - if not host: - host = "https://api.spectrocloud.com" - if project_uid and api_key: - if not host.startswith(("http://", "https://")): - host = f"https://{host}" - return project_uid, api_key, host - - env_file_path = _resolve_palette_env_file_path() - env_values = _read_env_file(env_file_path) - if not project_uid: - project_uid = _first_non_empty(env_values, project_uid_keys) - if not api_key: - api_key = _first_non_empty(env_values, api_key_keys) - if host == "https://api.spectrocloud.com": - parsed_host = _first_non_empty(env_values, host_keys) - if parsed_host: - host = parsed_host - - if not host.startswith(("http://", "https://")): - host = f"https://{host}" - - return project_uid, api_key, host - - -def _build_labels(tags: list[str]) -> dict[str, str]: - """Convert a list of tag strings into a Palette labels dict. - - Supported formats: - - "key:value" -> {"key": "value"} - - "single" -> {"single": "true"} - """ - labels: dict[str, str] = {} - for tag in tags: - if ":" in tag: - key, _, value = tag.partition(":") - labels[key.strip()] = value.strip() - else: - labels[tag.strip()] = "true" - return labels - - -@tool -def tag_cluster_for_review(cluster_uid: str, tags: list[str]) -> str: - """Tag a Palette cluster with the provided labels via HTTP PATCH. - - tags: list of strings in 'key:value' or 'single' format. - """ - if not cluster_uid.strip(): - return "STDOUT:\n\nSTDERR:\nMissing cluster UID.\nRC: 2" - - return _patch_palette_metadata( - resource_path=f"/v1/spectroclusters/{cluster_uid}/metadata", - missing_identifier_error="Missing cluster UID.", - tags=tags, - ) - - -@tool -def tag_cluster_profile_for_review(cluster_profile_uid: str, tags: list[str]) -> str: - """Tag a Palette cluster profile with the provided labels via HTTP PATCH. - - tags: list of strings in 'key:value' or 'single' format. - """ - if not cluster_profile_uid.strip(): - return "STDOUT:\n\nSTDERR:\nMissing cluster profile UID.\nRC: 2" - - return _patch_palette_metadata( - resource_path=f"/v1/clusterprofiles/{cluster_profile_uid}/metadata", - missing_identifier_error="Missing cluster profile UID.", - tags=tags, - ) - - -def _patch_palette_metadata( - resource_path: str, missing_identifier_error: str, tags: list[str] -) -> str: - if not resource_path.strip(): - return f"STDOUT:\n\nSTDERR:\n{missing_identifier_error}\nRC: 2" - - env_file_path = _resolve_palette_env_file_path() - project_uid, api_key, host = _resolve_palette_credentials() - if not project_uid: - return ( - "STDOUT:\n\nSTDERR:\n" - "Missing project UID. Checked env and " - f"{env_file_path} for project UID keys.\nRC: 2" - ) - if not api_key: - return ( - "STDOUT:\n\nSTDERR:\n" - "Missing API key. Checked env and " - f"{env_file_path} for API key keys.\nRC: 2" - ) - - payload: dict[str, Any] = { - "metadata": { - "labels": _build_labels(tags), - } - } - url = f"{host.rstrip('/')}{resource_path}" - request = urllib.request.Request( - url=url, - data=json.dumps(payload).encode("utf-8"), - headers={ - "ProjectUid": project_uid, - "Content-Type": "application/json", - "apiKey": api_key, - }, - method="PATCH", - ) - - try: - with urllib.request.urlopen(request, timeout=30) as response: - http_status = response.getcode() - body_output = response.read().decode("utf-8", errors="replace").rstrip() - stderr_output = "" - rc = 0 - except TimeoutError: - return "STDOUT:\n\nSTDERR:\nTagging request timed out after 30s.\nRC: 124" - except urllib.error.HTTPError as exc: - http_status = exc.code - body_output = exc.read().decode("utf-8", errors="replace").rstrip() - stderr_output = str(exc) - rc = 0 - except urllib.error.URLError as exc: - return ( - f"STDOUT:\n\nSTDERR:\nFailed to execute HTTP request: {exc.reason}\nRC: 127" - ) - except OSError as exc: - return f"STDOUT:\n\nSTDERR:\nFailed to execute HTTP request: {exc}\nRC: 127" - - is_success = str(http_status).startswith("2") - - return ( - f"STDOUT:\n{body_output}\n\n" - f"STDERR:\n{stderr_output}\n" - f"SUCCESS: {str(is_success).lower()}\n" - f"RC: {rc}" - )