Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d207e30
add interactive AI-powered deubgging tool `az aks agent`
mainred Jul 22, 2025
3e93c18
use holmesgpt official release
mainred Jul 29, 2025
feeff45
fix help examples
mainred Jul 29, 2025
dba652e
remove unnecessary flags
mainred Jul 29, 2025
9fd3d09
fix style lint
mainred Jul 29, 2025
8265cd1
address comments
mainred Jul 29, 2025
2d8590e
raise error on python <3.10
mainred Jul 29, 2025
15fbd99
fix lint
mainred Jul 29, 2025
d006fc1
test azure-mgmt-sql dependency
mainred Jul 29, 2025
5dee708
update flags
mainred Jul 31, 2025
041e0fc
refine AKS context system prompt
mainred Jul 31, 2025
05a818d
make rg and cluster name optional and update the system prompt
mainred Aug 1, 2025
d1efab2
use holmesgpt 0.12.4
mainred Aug 4, 2025
a3bbc45
Merge branch 'main' of github.com:Azure/azure-cli-extensions into aks…
mainred Aug 4, 2025
8e1c379
bump aks-preview version
mainred Aug 4, 2025
06ad10c
remove typer
mainred Aug 5, 2025
fd8ee72
fix custome-toolset flag
mainred Aug 7, 2025
bb7b57e
add validators for custom-toolsets and config-file
mainred Aug 8, 2025
c0d2ee4
expand the config and custom toolset path
mainred Aug 8, 2025
3b498cf
add disclaimer
mainred Aug 10, 2025
3e750fe
add config exmaple and remove custom-toolsets flag
mainred Aug 13, 2025
5da8c0f
Merge branch 'main' of github.com:Azure/azure-cli-extensions into aks…
mainred Aug 14, 2025
d473c41
add telemetry
mainred Aug 15, 2025
1bfdf24
add framework for AI agent
mainred Aug 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/aks-preview/azext_aks_preview/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -3885,3 +3885,58 @@
- name: Connect to a managed Kubernetes cluster using Azure Bastion with custom port and admin credentials.
text: az aks bastion -g MyResourceGroup --name MyManagedCluster --bastion MyBastionResource --port 50001 --admin
"""

helps['aks agent'] = """
type: command
short-summary: Run AI assistant to analyze and troubleshoot Kubernetes clusters.
long-summary: |-
This command allows you to ask questions about your Azure Kubernetes cluster and get answers using AI models.
Environment variables must be set to use the AI model, please refer to https://docs.litellm.ai/docs/providers to learn more about supported AI providers and models and required environment variables.
parameters:
- name: prompt
type: string
short-summary: Ask any question and get an answer using available tools.
- name: --config-file
type: string
short-summary: Path to configuration file.
- name: --max-steps
type: int
short-summary: Maximum number of steps the LLM can take to investigate the issue.
- name: --no-interactive
type: bool
short-summary: Disable interactive mode. When set, the agent will not prompt for input and will run in batch mode.
- name: --no-echo-request
type: bool
short-summary: Disable echoing back the question provided to AKS Agent in the output.
- name: --show-tool-output
type: bool
short-summary: Show the output of each tool that was called during the analysis.
- name: --refresh-toolsets
type: bool
short-summary: Refresh the toolsets status.

examples:
- name: Ask about pod issues in the cluster with Azure OpenAI
text: |-
export MODEL=azure/my-gpt4.1-deployment
export AZURE_API_BASE="https://my-azureopenai-service.openai.azure.com/"
export AZURE_API_VERSION="2025-01-01-preview"
export AZURE_API_KEY="sk-xxx"
az aks agent "Why are my pods not starting?"
- name: Ask about pod issues in the cluster with OpenAI
text: |-
export MODEL=gpt-4o
export OPENAI_API_KEY="sk-xxx"
az aks agent "Why are my pods not starting?"
text: az aks agent "Why are my pods not starting?"
- name: Run in non-interactive batch mode
text: az aks agent "Diagnose networking issues" --no-interactive --max-steps 5
- name: Show detailed tool output during analysis
text: az aks agent "Why is my service unavailable?" --show-tool-output
- name: Use custom configuration file
text: az aks agent "Check resource usage" --config-file /path/to/custom.config
- name: Run agent with no echo of the original question
text: az aks agent "What is the status of my cluster?" --no-echo-request
- name: Refresh toolsets to get the latest available tools
text: az aks agent --refresh-toolsets
"""
42 changes: 42 additions & 0 deletions src/aks-preview/azext_aks_preview/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
validate_nat_gateway_idle_timeout,
validate_nat_gateway_managed_outbound_ip_count,
)
from azure.cli.core.api import get_config_dir
from azure.cli.core.commands.parameters import (
edge_zone_type,
file_type,
Expand Down Expand Up @@ -2772,6 +2773,47 @@ def load_arguments(self, _):
action="store_true",
)

with self.argument_context("aks agent") as c:
c.positional("prompt", help="Ask any question and answer using available tools.")
c.argument(
"max_steps",
type=int,
default=10,
required=False,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You does set a default value of the corresponding parameter max_steps in function aks_agent, which is contrary to your declaration here. If you want to mark it as optional, please set a default value (like None or "") for the parameter.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not find the default value of max_steps in aks_agent.
Is it suggested to defaults a variable in this way?

help="Maximum number of steps the LLM can take to investigate the issue.",
)
c.argument(
"config_file",
type=str,
Comment thread
mainred marked this conversation as resolved.
Resolved
default=os.path.join(get_config_dir(), "aksAgent.config"),
required=False,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, it has no type and no default value conflict defined in aks_agent I think?

help="Path to the config file.",
)
c.argument(
"no_interactive",
help="Disable interactive mode. When set, the agent will not prompt for input and will run in batch mode.",
required=False,
Comment thread
mainred marked this conversation as resolved.
Resolved
action='store_true',
)
c.argument(
"no_echo_request",
help="Disable echoing back the question provided to AKS Agent in the output.",
required=False,
Comment thread
mainred marked this conversation as resolved.
Resolved
action='store_true',
)
c.argument(
"show_tool_output",
help="Show the output of each tool that was called.",
required=False,
Comment thread
mainred marked this conversation as resolved.
Resolved
action='store_true',
)
c.argument(
"refresh_toolsets",
help="Refresh the toolsets status.",
required=False,
Comment thread
mainred marked this conversation as resolved.
Resolved
action='store_true',
)


def _get_default_install_location(exe_name):
system = platform.system()
Expand Down
1 change: 1 addition & 0 deletions src/aks-preview/azext_aks_preview/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ def load_command_table(self, _):
"operation-abort", "aks_operation_abort", supports_no_wait=True
)
g.custom_command("bastion", "aks_bastion")
g.custom_command("agent", "aks_agent")

# AKS maintenance configuration commands
with self.command_group(
Expand Down
196 changes: 196 additions & 0 deletions src/aks-preview/azext_aks_preview/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
# pylint: disable=too-many-lines, disable=broad-except
import datetime
import json
import logging
import os
import os.path
from pathlib import Path
import platform
import socket
import ssl
import sys
import threading
import time
import uuid
import webbrowser

from azext_aks_preview._client_factory import (
Expand Down Expand Up @@ -4383,3 +4387,195 @@ def aks_bastion(cmd, client, resource_group_name, name, bastion=None, port=None,
)
finally:
aks_batsion_clean_up()
Comment thread
mainred marked this conversation as resolved.
Resolved


def aks_agent(
cmd,
client,
resource_group_name,
name,
prompt,
max_steps,
config_file,
no_interactive=False,
no_echo_request=False,
show_tool_output=False,
refresh_toolsets=False,
):
'''
Comment thread
mainred marked this conversation as resolved.
Resolved
Interact with the AKS agent using a prompt or piped input.

:param prompt: The prompt to send to the agent.
:type prompt: str
:param interactive: Whether to run in interactive mode.
:type interactive: bool
:param max_steps: Maximum number of steps to take.
:type max_steps: int
:param config_file: Path to the config file.
:type config_file: str
:param no_interactive: Disable interactive mode.
Comment thread
mainred marked this conversation as resolved.
Resolved
:type no_interactive: bool
:param no_echo_request: Disable echoing back the question provided to AKS Agent in the output.
:type no_echo_request: bool
:param show_tool_output: Whether to show tool output.
:type show_tool_output: bool
:param refresh_toolsets: Refresh the toolsets status.
:type refresh_toolsets: bool
'''

# reverse the value of the variables so that
Comment thread
mainred marked this conversation as resolved.
Resolved
interactive = not no_interactive
echo = not no_echo_request

# Holmes library allows the user to specify the agent name through environment variable before loading the library.
os.environ["AGENT_NAME"] = "AKS AGENT"
# NOTE(mainred): we need to disable INFO logs from LiteLLM before LiteLLM library is loaded, to avoid logging the
# debug logs from heading of LiteLLM.
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
import typer
from holmes.config import Config
from holmes.core.prompt import build_initial_ask_messages
from holmes.interactive import run_interactive_loop
from holmes.plugins.destinations import DestinationType
from holmes.plugins.interfaces import Issue
from holmes.plugins.prompts import load_and_render_prompt
from holmes.utils.console.logging import init_logging
from holmes.utils.console.result import handle_result

# NOTE(mainred): holmes leverage the log handler RichHandler to provide colorful, readable and well-formatted logs
# making the interactive mode more user-friendly.
# And we removed exising log handlers to avoid duplicate logs.
# Also make the console log consistent, we remove the telemetry and data logger to skip redundant logs.
def init_log():
logging.getLogger("telemetry.main").setLevel(logging.WARNING)
logging.getLogger("telemetry.process").setLevel(logging.WARNING)
logging.getLogger("telemetry.save").setLevel(logging.WARNING)
logging.getLogger("telemetry.client").setLevel(logging.WARNING)
logging.getLogger("az_command_data_logger").setLevel(logging.WARNING)
# TODO: make log verbose configurable, currently disbled by [].
return init_logging([])

console = init_log()

# Detect and read piped input
piped_data = None
if not sys.stdin.isatty():
piped_data = sys.stdin.read().strip()
if interactive:
console.print(
"[bold yellow]Interactive mode disabled when reading piped input[/bold yellow]"
)
interactive = False

config_file = Path(config_file)
config = Config.load_from_file(
config_file,
max_steps=max_steps,
)

ai = config.create_console_toolcalling_llm(
dal=None,
refresh_toolsets=refresh_toolsets,
)
template_context = {
"toolsets": ai.tool_executor.toolsets,
"runbooks": config.get_runbook_catalog(),
}

if not prompt and not interactive and not piped_data:
raise typer.BadParameter(
"Either the 'prompt' argument must be provided (unless using --interactive mode)."
)

# Handle piped data
if piped_data:
if prompt:
# User provided both piped data and a prompt
prompt = f"Here's some piped output:\n\n{piped_data}\n\n{prompt}"
else:
# Only piped data, no prompt - ask what to do with it
prompt = f"Here's some piped output:\n\n{piped_data}\n\nWhat can you tell me about this output?"

if echo and not interactive and prompt:
console.print("[bold yellow]User:[/bold yellow] " + prompt)

# TODO: extend the system prompt with AKS context
system_prompt = "builtin://generic_ask.jinja2"
system_prompt_rendered = load_and_render_prompt(system_prompt, template_context)

subscription_id = get_subscription_id(cmd.cli_ctx)

aks_template_context = {
"cluster_name": name,
"resource_group": resource_group_name,
"subscription_id": subscription_id,
}

aks_context_prompt = """
# Azure Kubernetes Service (AKS)

You are specifically working with Azure Kubernetes Service (AKS) clusters. All investigations and troubleshooting should be performed on the AKS cluster. When troubleshooting AKS, you should consider both Azure resources and Kubernetes resources.

The current provided AKS context is as follow:
cluster_name: {{cluster_name}}
resource_group: {{resource_group}}
subscription_id: {{subscription_id}}

## Prerequisites
### AKS cluster name is under the resource group and subscription specified

You should check if the AKS cluster {{cluster_name}} can be found under resource group {{resource_group}} and subscription {{subscription_id}}.
If not, you should prompt to the user to specify correct cluster name, resource group and subscription ID.

## AKS cluster is in the current kubeconfig context
If the current kubeconfig context is not set to the AKS cluster {{cluster_name}}, you should download the kubeconfig credential with the cluster name {{cluster_name}}, resource group name {{resource_group}} and subscription ID {{subscription_id}}.
If the current kubeconfig context is set to the AKS cluster {{cluster_name}}, you should proceed with the investigation and troubleshooting.
"""
aks_context_prompt = load_and_render_prompt(aks_context_prompt, aks_template_context)
system_prompt_rendered += aks_context_prompt

# Variables not exposed to the user.
# Adds a prompt for post processing.
post_processing_prompt = None
# File to append to prompt
include_file = None
# TODO: add refresh-toolset to refresh the toolset if it has changed
if interactive:
run_interactive_loop(
ai,
console,
system_prompt_rendered,
prompt,
post_processing_prompt,
include_file,
show_tool_output=show_tool_output,
)
return

messages = build_initial_ask_messages(
console,
system_prompt_rendered,
prompt,
include_file,
)

response = ai.call(messages)

messages = response.messages # type: ignore # Update messages with the full history
Comment thread
mainred marked this conversation as resolved.
Resolved

issue = Issue(
id=str(uuid.uuid4()),
name=prompt,
source_type="holmes-ask",
raw={"prompt": prompt, "full_conversation": messages},
source_instance_id=socket.gethostname(),
)
handle_result(
response,
console,
DestinationType.CLI,
config,
issue,
show_tool_output,
False,
)
6 changes: 4 additions & 2 deletions src/aks-preview/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from codecs import open as open1

from setuptools import setup, find_packages
from setuptools import find_packages, setup

VERSION = "18.0.0b21"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FumingZhang I don't need to new version for this change yet until some other changes are merged. How should I update the change into the next release's change log?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To release a new version,

  1. update the version number in this file (setup.py)
  2. add a release note for the new version in file history.rst


Expand All @@ -23,7 +23,9 @@
"License :: OSI Approved :: MIT License",
]

DEPENDENCIES = []
DEPENDENCIES = [
"holmesgpt==0.12.3"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does not always take effect, better test this thoroughly

@FumingZhang FumingZhang Jul 29, 2025 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

holmesgpt==0.12.3; python_version >= "3.10"
you may need this as homesgpt does not support python 3.10, and in this case, you need to do a validation on python version in your command and prompt user to upgrade their python to 3.10+ if hey are using python 3.9

ERROR: Ignored the following versions that require a different python version: 0.11.5 Requires-Python <4.0,>=3.10; 0.12.0 Requires-Python <4.0,>=3.10; 0.12.0a0 Requires-Python <4.0,>=3.10; 0.12.1 Requires-Python <4.0,>=3.10; 0.12.2a0 Requires-Python <4.0,>=3.10; 0.12.2a1 Requires-Python <4.0,>=3.10; 0.12.2a2 Requires-Python <4.0,>=3.10; 0.12.3 Requires-Python <4.0,>=3.10; 0.12.3a1 Requires-Python <4.0,>=3.10
ERROR: Could not find a version that satisfies the requirement holmesgpt==0.12.3 (from aks-preview) (from versions: none)
ERROR: No matching distribution found for holmesgpt==0.12.3

https://dev.azure.com/azclitools/public/_build/results?buildId=263871&view=logs&j=155127ab-52f1-52e7-0e45-625d9d7884a4&t=4bb67950-ef03-52d4-44f5-d1a8cd56c919&l=151

]

with open1("README.rst", "r", encoding="utf-8") as f:
README = f.read()
Expand Down