From 05919f23cae31cec109a4498a1c5e4512cccb49e Mon Sep 17 00:00:00 2001 From: Nicolas Herment Date: Mon, 20 Jan 2025 08:09:34 +0100 Subject: [PATCH 1/4] feat: add structured sections to AI output --- src/robusta/core/playbooks/internal/ai_integration.py | 2 +- src/robusta/core/reporting/holmes.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/robusta/core/playbooks/internal/ai_integration.py b/src/robusta/core/playbooks/internal/ai_integration.py index 4c580b266..aea7c91ce 100644 --- a/src/robusta/core/playbooks/internal/ai_integration.py +++ b/src/robusta/core/playbooks/internal/ai_integration.py @@ -123,7 +123,7 @@ def holmes_workload_health(event: ExecutionBaseEvent, params: HolmesWorkloadHeal analysis = json.loads(holmes_result.analysis) healthy = analysis.get("workload_healthy") except Exception: - logging.exception("Error in holmes response format, analysis did not return the expecrted json format.") + logging.exception("Error in holmes response format, analysis did not return the expected json format.") pass if params.silent_healthy and healthy: diff --git a/src/robusta/core/reporting/holmes.py b/src/robusta/core/reporting/holmes.py index 78c0c86b0..f8f3b956e 100644 --- a/src/robusta/core/reporting/holmes.py +++ b/src/robusta/core/reporting/holmes.py @@ -50,6 +50,7 @@ class ToolCallResult(BaseModel): class HolmesResult(BaseModel): tool_calls: Optional[List[ToolCallResult]] = None analysis: Optional[str] = None + sections: Optional[Dict[str, str]] = None instructions: List[str] = [] From 6490239b1a29fe90f9447ceca219ba2291e85b1f Mon Sep 17 00:00:00 2001 From: Nicolas Herment Date: Tue, 21 Jan 2025 07:51:12 +0100 Subject: [PATCH 2/4] feat: add ability for client to set expected sections for Holmes investigations --- src/robusta/core/model/base_params.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/robusta/core/model/base_params.py b/src/robusta/core/model/base_params.py index f0ebd1688..46f605b28 100644 --- a/src/robusta/core/model/base_params.py +++ b/src/robusta/core/model/base_params.py @@ -99,6 +99,7 @@ class AIInvestigateParams(HolmesParams): :var runbooks: List of human readable recommended runbooks that holmes can use for the investigation. :var ask: Override question to ask holmes :var context: Additional information that can assist with the investigation + :var sections: Sections that Holmes should return. Dictionary where the key is the section's title and the value a description for Holmes (LLM) :example ask: What are all the issues in my cluster right now? :example runbooks: ["Try to get the pod logs and find errors", "get the pod yaml and check if there are finalizers"] @@ -109,6 +110,7 @@ class AIInvestigateParams(HolmesParams): runbooks: Optional[List[str]] ask: Optional[str] context: Optional[Dict[str, Any]] + sections: Optional[Dict[str, str]] = None class HolmesToolsResult(BaseModel): From d0a691561b8a35e41647c6cf31dcf6e1b90a87d4 Mon Sep 17 00:00:00 2001 From: Nicolas Herment Date: Tue, 21 Jan 2025 08:06:53 +0100 Subject: [PATCH 3/4] feat: add ability for client to set expected sections for Holmes investigations --- src/robusta/core/playbooks/internal/ai_integration.py | 1 + src/robusta/core/reporting/holmes.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/robusta/core/playbooks/internal/ai_integration.py b/src/robusta/core/playbooks/internal/ai_integration.py index aea7c91ce..40184b114 100644 --- a/src/robusta/core/playbooks/internal/ai_integration.py +++ b/src/robusta/core/playbooks/internal/ai_integration.py @@ -58,6 +58,7 @@ def ask_holmes(event: ExecutionBaseEvent, params: AIInvestigateParams): context=params.context if params.context else {}, include_tool_calls=True, include_tool_call_results=True, + sections=params.sections ) result = requests.post(f"{holmes_url}/api/investigate", data=holmes_req.json()) result.raise_for_status() diff --git a/src/robusta/core/reporting/holmes.py b/src/robusta/core/reporting/holmes.py index f8f3b956e..929a19ae9 100644 --- a/src/robusta/core/reporting/holmes.py +++ b/src/robusta/core/reporting/holmes.py @@ -19,6 +19,7 @@ class HolmesRequest(BaseModel): context: Dict[str, Any] include_tool_calls: bool = False include_tool_call_results: bool = False + sections: Optional[Dict[str, str]] = None class HolmesConversationRequest(BaseModel): From a5aa566cc9503a8cb651a31b0fea7aab08a43fec Mon Sep 17 00:00:00 2001 From: Nicolas Herment Date: Mon, 27 Jan 2025 16:32:50 +0100 Subject: [PATCH 4/4] fix: support for None sections and return structured data --- src/robusta/core/reporting/holmes.py | 4 ++-- .../core/sinks/robusta/dal/model_conversion.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/robusta/core/reporting/holmes.py b/src/robusta/core/reporting/holmes.py index 929a19ae9..b592f6cba 100644 --- a/src/robusta/core/reporting/holmes.py +++ b/src/robusta/core/reporting/holmes.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel @@ -51,7 +51,7 @@ class ToolCallResult(BaseModel): class HolmesResult(BaseModel): tool_calls: Optional[List[ToolCallResult]] = None analysis: Optional[str] = None - sections: Optional[Dict[str, str]] = None + sections: Optional[Dict[str, Union[str, None]]] = None instructions: List[str] = [] diff --git a/src/robusta/core/sinks/robusta/dal/model_conversion.py b/src/robusta/core/sinks/robusta/dal/model_conversion.py index e8ba5b91c..3c659c0c6 100644 --- a/src/robusta/core/sinks/robusta/dal/model_conversion.py +++ b/src/robusta/core/sinks/robusta/dal/model_conversion.py @@ -95,6 +95,7 @@ def append_to_structured_data_tool_calls(tool_calls: List[ToolCallResult], struc data_obj["metadata"] = {"description": tool_call.description, "tool_name": tool_call.tool_name} structured_data.append(data_obj) + @staticmethod def add_ai_chat_data(structured_data: List[Dict], block: HolmesChatResultsBlock): structured_data.append( @@ -117,6 +118,8 @@ def add_ai_chat_data(structured_data: List[Dict], block: HolmesChatResultsBlock) @staticmethod def add_ai_analysis_data(structured_data: List[Dict], block: HolmesResultsBlock): + if not block.holmes_result: + return structured_data.append( { "type": "markdown", @@ -125,6 +128,19 @@ def add_ai_analysis_data(structured_data: List[Dict], block: HolmesResultsBlock) } ) ModelConversion.append_to_structured_data_tool_calls(block.holmes_result.tool_calls, structured_data) + if block.holmes_result.sections and len(block.holmes_result.sections) > 0: + transformed_sections = {} + for section_title, section_content in block.holmes_result.sections.items(): + if section_content: + transformed_sections[section_title] = Transformer.to_github_markdown(section_content) + + structured_data.append( + { + "type": "markdown", + "metadata": {"type": "ai_investigation_sections", "createdAt": datetime_to_db_str(datetime.now())}, + "data": transformed_sections, + } + ) structured_data.append({"type": "list", "data": block.holmes_result.instructions})