|
6 | 6 | # pylint: disable=too-many-lines, disable=broad-except |
7 | 7 | import datetime |
8 | 8 | import json |
| 9 | +import logging |
9 | 10 | import os |
10 | 11 | import os.path |
| 12 | +from pathlib import Path |
11 | 13 | import platform |
12 | | -import ssl |
| 14 | +import socket |
13 | 15 | import sys |
14 | 16 | import threading |
15 | 17 | import time |
| 18 | +import typer |
| 19 | +import uuid |
16 | 20 | import webbrowser |
17 | 21 |
|
18 | 22 | from azext_aks_preview._client_factory import ( |
@@ -4333,3 +4337,204 @@ def aks_loadbalancer_rebalance_nodes( |
4333 | 4337 | } |
4334 | 4338 |
|
4335 | 4339 | return aks_loadbalancer_rebalance_internal(managed_clusters_client, parameters) |
| 4340 | + |
| 4341 | + |
| 4342 | +def aks_agent( |
| 4343 | + cmd, |
| 4344 | + client, |
| 4345 | + resource_group_name, |
| 4346 | + name, |
| 4347 | + prompt, |
| 4348 | + api_key, |
| 4349 | + model, |
| 4350 | + max_steps, |
| 4351 | + config_file, |
| 4352 | + no_interactive=False, |
| 4353 | + no_echo_request=False, |
| 4354 | + show_tool_output=False, |
| 4355 | + refresh_toolsets=False, |
| 4356 | + ): |
| 4357 | + # add description for the function and variables |
| 4358 | + ''' |
| 4359 | + Interact with the AKS agent using a prompt or piped input. |
| 4360 | +
|
| 4361 | + :param prompt: The prompt to send to the agent. |
| 4362 | + :type prompt: str |
| 4363 | + :param api_key: API key for authentication. |
| 4364 | + :type api_key: str |
| 4365 | + :param model: Model to use for the LLM. |
| 4366 | + :type model: str |
| 4367 | + :param interactive: Whether to run in interactive mode. |
| 4368 | + :type interactive: bool |
| 4369 | + :param max_steps: Maximum number of steps to take. |
| 4370 | + :type max_steps: int |
| 4371 | + :param config_file: Path to the config file. |
| 4372 | + :type config_file: str |
| 4373 | + :param no_interactive: Disable interactive mode. |
| 4374 | + :type no_interactive: bool |
| 4375 | + :param no_echo_request: Disable echoing back the question provided to AKS Agent in the output. |
| 4376 | + :type no_echo_request: bool |
| 4377 | + :param show_tool_output: Whether to show tool output. |
| 4378 | + :type show_tool_output: bool |
| 4379 | + :param refresh_toolsets: Refresh the toolsets status. |
| 4380 | + :type refresh_toolsets: bool |
| 4381 | + ''' |
| 4382 | + |
| 4383 | + # reverse the value of the variables so that |
| 4384 | + interactive = not no_interactive |
| 4385 | + echo = not no_echo_request |
| 4386 | + |
| 4387 | + # Holmes library allows the user to specify the agent name through environment variable before loading the library. |
| 4388 | + os.environ["AGENT_NAME"] = "AKS AGENT" |
| 4389 | + # 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. |
| 4390 | + logging.getLogger("LiteLLM").setLevel(logging.WARNING) |
| 4391 | + from holmes.config import Config |
| 4392 | + from holmes.core.prompt import build_initial_ask_messages |
| 4393 | + from holmes.interactive import run_interactive_loop |
| 4394 | + from holmes.plugins.destinations import DestinationType |
| 4395 | + from holmes.plugins.interfaces import Issue |
| 4396 | + from holmes.plugins.prompts import load_and_render_prompt |
| 4397 | + from holmes.utils.console.logging import init_logging |
| 4398 | + from holmes.utils.console.result import handle_result |
| 4399 | + |
| 4400 | + # NOTE(mainred): holmes leverage the log handler RichHandler to provide colorful, readable and well-formatted logs |
| 4401 | + # making the interactive mode more user-friendly. |
| 4402 | + # And we removed exising log handlers to avoid duplicate logs. |
| 4403 | + # Also make the console log consistent, we remove the telemetry and data logger to skip redundant logs. |
| 4404 | + def init_log(): |
| 4405 | + logging.getLogger("telemetry.main").setLevel(logging.WARNING) |
| 4406 | + logging.getLogger("telemetry.process").setLevel(logging.WARNING) |
| 4407 | + logging.getLogger("telemetry.save").setLevel(logging.WARNING) |
| 4408 | + logging.getLogger("telemetry.client").setLevel(logging.WARNING) |
| 4409 | + logging.getLogger("az_command_data_logger").setLevel(logging.WARNING) |
| 4410 | + # TODO: make log verbose configurable, currently disbled by []. |
| 4411 | + return init_logging([]) |
| 4412 | + |
| 4413 | + console = init_log() |
| 4414 | + |
| 4415 | + # Detect and read piped input |
| 4416 | + piped_data = None |
| 4417 | + if not sys.stdin.isatty(): |
| 4418 | + piped_data = sys.stdin.read().strip() |
| 4419 | + if interactive: |
| 4420 | + console.print( |
| 4421 | + "[bold yellow]Interactive mode disabled when reading piped input[/bold yellow]" |
| 4422 | + ) |
| 4423 | + interactive = False |
| 4424 | + |
| 4425 | + config_file = Path(config_file) |
| 4426 | + config = Config.load_from_file( |
| 4427 | + config_file, |
| 4428 | + api_key=api_key, |
| 4429 | + model=model, |
| 4430 | + max_steps=max_steps, |
| 4431 | + ) |
| 4432 | + |
| 4433 | + ai = config.create_console_toolcalling_llm( |
| 4434 | + dal=None, |
| 4435 | + refresh_toolsets=refresh_toolsets, |
| 4436 | + ) |
| 4437 | + template_context = { |
| 4438 | + "toolsets": ai.tool_executor.toolsets, |
| 4439 | + "runbooks": config.get_runbook_catalog(), |
| 4440 | + } |
| 4441 | + |
| 4442 | + if not prompt and not interactive and not piped_data: |
| 4443 | + raise typer.BadParameter( |
| 4444 | + "Either the 'prompt' argument must be provided (unless using --interactive mode)." |
| 4445 | + ) |
| 4446 | + |
| 4447 | + # Handle piped data |
| 4448 | + if piped_data: |
| 4449 | + if prompt: |
| 4450 | + # User provided both piped data and a prompt |
| 4451 | + prompt = f"Here's some piped output:\n\n{piped_data}\n\n{prompt}" |
| 4452 | + else: |
| 4453 | + # Only piped data, no prompt - ask what to do with it |
| 4454 | + prompt = f"Here's some piped output:\n\n{piped_data}\n\nWhat can you tell me about this output?" |
| 4455 | + |
| 4456 | + if echo and not interactive and prompt: |
| 4457 | + console.print("[bold yellow]User:[/bold yellow] " + prompt) |
| 4458 | + |
| 4459 | + # TODO: extend the system prompt with AKS context |
| 4460 | + system_prompt= "builtin://generic_ask.jinja2" |
| 4461 | + system_prompt_rendered = load_and_render_prompt(system_prompt, template_context) |
| 4462 | + |
| 4463 | + subscription_id = get_subscription_id(cmd.cli_ctx) |
| 4464 | + |
| 4465 | + aks_template_context = { |
| 4466 | + "cluster_name": name, |
| 4467 | + "resource_group": resource_group_name, |
| 4468 | + "subscription_id": subscription_id, |
| 4469 | + } |
| 4470 | + |
| 4471 | + aks_context_prompt = """ |
| 4472 | +# Azure Kubernetes Service (AKS) |
| 4473 | +
|
| 4474 | +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. |
| 4475 | +
|
| 4476 | +The current provided AKS context is as follow: |
| 4477 | +cluster_name: {{cluster_name}} |
| 4478 | +resource_group: {{resource_group}} |
| 4479 | +subscription_id: {{subscription_id}} |
| 4480 | +
|
| 4481 | +## Prerequisites |
| 4482 | +### AKS cluster name is under the resource group and subscription specified |
| 4483 | +
|
| 4484 | +You should check if the AKS cluster {{cluster_name}} can be found under resource group {{resource_group}} and subscription {{subscription_id}}. |
| 4485 | +If not, you should prompt to the user to specify correct cluster name, resource group and subscription ID. |
| 4486 | +
|
| 4487 | +## AKS cluster is in the current kubeconfig context |
| 4488 | +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}}. |
| 4489 | +If the current kubeconfig context is set to the AKS cluster {{cluster_name}}, you should proceed with the investigation and troubleshooting. |
| 4490 | +""" |
| 4491 | + aks_context_prompt = load_and_render_prompt(aks_context_prompt, aks_template_context) |
| 4492 | + system_prompt_rendered += aks_context_prompt |
| 4493 | + |
| 4494 | + ## Variables not exposed to the user. |
| 4495 | + # Adds a prompt for post processing. |
| 4496 | + post_processing_prompt = None |
| 4497 | + # File to append to prompt |
| 4498 | + include_file = None |
| 4499 | + # TODO: add refresh-toolset to refresh the toolset if it has changed |
| 4500 | + if interactive: |
| 4501 | + run_interactive_loop( |
| 4502 | + ai, |
| 4503 | + console, |
| 4504 | + system_prompt_rendered, |
| 4505 | + prompt, |
| 4506 | + post_processing_prompt, |
| 4507 | + include_file, |
| 4508 | + show_tool_output=show_tool_output, |
| 4509 | + ) |
| 4510 | + return |
| 4511 | + |
| 4512 | + messages = build_initial_ask_messages( |
| 4513 | + console, |
| 4514 | + system_prompt_rendered, |
| 4515 | + prompt, |
| 4516 | + include_file, |
| 4517 | + ) |
| 4518 | + |
| 4519 | + response = ai.call(messages) |
| 4520 | + |
| 4521 | + |
| 4522 | + messages = response.messages # type: ignore # Update messages with the full history |
| 4523 | + |
| 4524 | + issue = Issue( |
| 4525 | + id=str(uuid.uuid4()), |
| 4526 | + name=prompt, |
| 4527 | + source_type="holmes-ask", |
| 4528 | + raw={"prompt": prompt, "full_conversation": messages}, |
| 4529 | + source_instance_id=socket.gethostname(), |
| 4530 | + ) |
| 4531 | + handle_result( |
| 4532 | + response, |
| 4533 | + console, |
| 4534 | + DestinationType.CLI, |
| 4535 | + config, |
| 4536 | + issue, |
| 4537 | + show_tool_output, |
| 4538 | + False, |
| 4539 | + ) |
| 4540 | + |
0 commit comments