Skip to content

feat: Add experimental AG-UI supported chat endpoint and PPL query assist - #1035

Merged
arikalon1 merged 24 commits into
HolmesGPT:masterfrom
kylehounslow:master
Oct 10, 2025
Merged

arikalon1 merged 24 commits into
HolmesGPT:masterfrom
kylehounslow:master

Conversation

@kylehounslow

@kylehounslow kylehounslow commented Oct 6, 2025 •

Copy link
Copy Markdown
Contributor

Summary

This PR introduces an experimental AG-UI chat server for HolmesGPT. The primary use-case is to support AI-powered data exploration and root-cause-analysis capabilities directly within observability platforms like OpenSearch Dashboards. It also adds experimental support for query assist with OpenSearch Piped Processing Language (PPL).

Resolves #889

ExampleOps demo video

video source

Why is this change necessary?

Platforms like OpenSearch Dashboards are integrating AI-powered data exploration and problem diagnosis capabilities directly into their core user experience, making open-source observability more intelligent and automated (see RFCs here and here). By implementing AG-UI support, HolmesGPT can be deployed as a modular, context-aware agent that seamlessly integrates with the frontend architecture. This design approach minimizes implementation overhead while maintaining deep integration with the platform's UI components and data structures.

Issues

Summary of Changes

  • Add experimental AG-UI chat server implementation at experimental/ag-ui/server.py
  • Add AG-UI toolset loader in config.py (loads same toolsets as existing console)
  • Add experimental PPL query assist tool
  • Add demo observability front-end at experimental/ag-ui/front-end/
  • chore: add ag-ui dependency to pyproject.toml

Notes for Reviewers

  • This PR appears large at first glance, but the example front-end contained in experimental/ag-ui/front-end/ is for demonstration purposes only. Feel free to skim this or omit altogether for this review.
  • Main server implementation is at experimental/ag-ui/server.py
  • PPL query assist toolset is at holmes/plugins/toolsets/opensearch/opensearch_query_assist.py

Testing

Unit Testing

poetry run pytest tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py

Functional Testing

An example observability front-end was built to demonstrate the AG-UI functionality.
Setup details are at experimental/ag-ui/README.md. Start a prometheus server and opensearch cluster (e.g. using opentelemetry-demo) and:

cd experimental/ag-ui
poetry run python server.py
cd experimental/ag-ui/front-end 
yarn install 
yarn start

@CLAassistant

CLAassistant commented Oct 6, 2025 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Oct 6, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds an experimental AG-UI integration: a FastAPI SSE AG-UI server, a React ExampleOps front-end with ChatAssistant and observability pages, OpenSearch PPL query-assist toolset and templates, tests, Holmes config wiring for AG-UI tool executor/tool-calling, Docker copy, and documentation/env files.

Changes

Cohort / File(s) Summary
Docs
experimental/ag-ui/README.md, experimental/ag-ui/front-end/README.md
New documentation and Quick Start for AG-UI server and ExampleOps front-end.
Front-end scaffolding & config
experimental/ag-ui/front-end/.env.example, experimental/ag-ui/front-end/.gitignore, experimental/ag-ui/front-end/.nvmrc, experimental/ag-ui/front-end/package.json, experimental/ag-ui/front-end/public/index.html, experimental/ag-ui/front-end/tsconfig.json, experimental/ag-ui/front-end/src/index.tsx, experimental/ag-ui/front-end/src/index.css
Adds React/TypeScript app boilerplate, env example, Node version pin, gitignore, package manifest, HTML entry, TS config, and base styles.
Front-end app & components
experimental/ag-ui/front-end/src/App.tsx, experimental/ag-ui/front-end/src/components/MainContent.tsx, experimental/ag-ui/front-end/src/components/ChatAssistant.tsx, experimental/ag-ui/front-end/src/components/ErrorBoundary.tsx
Implements app routing/state, per-page query handling, AG-UI-backed ChatAssistant with tool-call integration, and an ErrorBoundary component.
Visualizations
experimental/ag-ui/front-end/src/components/GraphVisualization.tsx, experimental/ag-ui/front-end/src/components/LogsVisualization.tsx, experimental/ag-ui/front-end/src/components/GraphVisualization.css, experimental/ag-ui/front-end/src/components/LogsVisualization.css
Adds GraphVisualization and LogsVisualization components with accompanying styles and custom legend/CSV export logic.
Chat styles
experimental/ag-ui/front-end/src/components/ChatAssistant.css, experimental/ag-ui/front-end/src/App.css
Adds comprehensive styling for chat UI, layout, and platform pages (responsive/accessibility).
AG-UI server
experimental/ag-ui/server-agui.py, Dockerfile
New FastAPI AG-UI endpoints (health, POST /api/agui/chat streaming SSE events, GET /api/model) and Docker COPY for the server file.
Holmes core config & dependency
holmes/config.py, pyproject.toml
Adds AG-UI-specific tool executor caching and create_agui_toolcalling_llm helper; adds ag-ui-protocol dependency.
OpenSearch PPL query assist
holmes/plugins/toolsets/__init__.py, holmes/plugins/toolsets/opensearch/opensearch_query_assist.py, holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2, holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2
New PplQueryAssistTool and OpenSearchQueryAssistToolset, instruction templates and PPL documentation snippets, registered in toolset loader.
Tests
tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
Adds tests covering tool/toolset initialization, invocation, one-liners, and instruction template reload behavior.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as Front-end ChatAssistant
  participant FE as Front-end MainContent
  participant AG as AG-UI Server (FastAPI)
  participant HL as Holmes (LLM + Tool Executor)
  participant DS as Data Sources (Prometheus/OpenSearch)

  UI->>AG: POST /api/agui/chat (RunAgentInput) [SSE]
  AG-->>UI: RUN_STARTED
  AG->>HL: Build runner (messages + toolset)
  HL-->>AG: Stream TEXT tokens
  AG-->>UI: TEXT_MESSAGE_START / TEXT_MESSAGE_CONTENT / TEXT_MESSAGE_END
  alt LLM emits tool call
    HL-->>AG: TOOL_CALL_START (execute_promql_query / execute_ppl_query)
    AG-->>UI: TOOL_CALL_START (forwarded)
    UI->>FE: onExecutePromQLQuery / onExecutePPLQuery
    FE->>DS: Query Prometheus/OpenSearch
    DS-->>FE: Results
    FE-->>UI: TOOL_CALL_END (result)
    UI-->>AG: TOOL_CALL_END (ack/result)
  end
  HL-->>AG: RUN_FINISHED / RUN_ERROR
  AG-->>UI: RUN_FINISHED / RUN_ERROR
Loading
sequenceDiagram
  autonumber
  participant UI as ChatAssistant
  participant FE as MainContent
  participant PR as Prometheus
  participant OS as OpenSearch

  UI->>FE: onExecutePromQLQuery(query)
  FE->>PR: /api/v1/query_range (PromQL)
  PR-->>FE: Time-series data
  FE-->>UI: GraphData (GraphVisualization)

  UI->>FE: onExecutePPLQuery(query)
  FE->>OS: _plugins/_ppl (PPL)
  OS-->>FE: Logs data
  FE-->>UI: LogData (LogsVisualization)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Implement LLM Model Registry #954 — Alters Config/LLM/tool-executor creation paths that overlap the new create_agui_tool_executor/create_agui_toolcalling_llm additions; high code-level relevance.

Suggested reviewers

  • arikalon1
  • moshemorad
  • nherment

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly identifies the two main features introduced by the PR: an experimental AG-UI chat endpoint and PPL query assist support. It is concise and specific, avoiding irrelevant details or file listings. This phrasing makes it easy for reviewers to understand the PR’s primary purpose at a glance.
Linked Issues Check ✅ Passed The PR adds a dedicated FastAPI AG-UI chat server that streams structured events over SSE, maps agent outputs and tool calls to the required AG-UI event types, and introduces a PPL query assist toolset, satisfying the core objectives of issue #889. It implements a separate HTTP server mode instead of a CLI flag, which aligns with the suggested design for backward compatibility. This coverage demonstrates that the linked issue’s requirements for protocol support, event mapping, and transport mode are fully met.
Out of Scope Changes Check ✅ Passed All changes in this PR, including the server implementation, PPL toolset, configuration updates, documentation, and demo front-end, directly support the AG-UI protocol and PPL query assist objectives. There are no modifications that fall outside the scope of enabling experimental AG-UI support and query assistance. The included demo assets and docs reinforce the core functionality rather than adding unrelated features.
Description Check ✅ Passed The description directly relates to the changeset by summarizing the addition of the experimental AG-UI chat server, PPL query assist support, and the associated demo front-end. It outlines why the change is necessary, references the linked issue, and provides testing instructions that match the implementation. The level of detail is appropriate and clearly tied to the code changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (20)
holmes/config.py (1)

293-304: Add docstring for consistency.

The method create_agui_toolcalling_llm lacks a docstring, while similar methods like create_console_toolcalling_llm (line 280-291) include documentation. Adding a docstring would improve consistency and maintainability.

Apply this diff to add a docstring:

     def create_agui_toolcalling_llm(
             self,
             dal: Optional["SupabaseDal"] = None,
             model: Optional[str] = None,
             tracer=None,
     ) -> "ToolCallingLLM":
+        """
+        Creates a ToolCallingLLM instance configured for AG-UI usage.
+        
+        Args:
+            dal: Optional SupabaseDal instance for data access
+            model: Optional model key to override the default model
+            tracer: Optional tracer for observability
+            
+        Returns:
+            ToolCallingLLM configured with AG-UI toolsets
+        """
         tool_executor = self.create_agui_tool_executor(dal)
         from holmes.core.tool_calling_llm import ToolCallingLLM

         return ToolCallingLLM(
             tool_executor, self.max_steps, self._get_llm(model, tracer)
         )
tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (1)

286-315: Schema consistency: query should be string OR object/array, not string-with-items.

The test enforces parameters["query"].type == "string" and also expects nested items.properties. That mixes JSON-schema concepts (items belong to arrays). Consider either:

  • keep type: "string" and remove items; or
  • change to type: "object"/"array" and keep items/properties.

This will make tools more interoperable (e.g., UI autogeneration).

Based on learnings

experimental/ag-ui/front-end/src/components/MainContent.tsx (6)

4-4: Avoid duplicating ObservabilityPage type.

Import the shared type from App.tsx (or a common types module) to prevent drift.

Apply this diff:

-import GraphVisualization from './GraphVisualization';
-import LogsVisualization from './LogsVisualization';
-type ObservabilityPage = 'metrics' | 'logs' | 'traces';
+import GraphVisualization from './GraphVisualization';
+import LogsVisualization from './LogsVisualization';
+import type { ObservabilityPage } from '../App';

41-43: Remove unused refs.

currentQueryRef and abortControllerRef are defined but never used.

Apply this diff:

-  const currentQueryRef = React.useRef<string>('');
-  const abortControllerRef = React.useRef<AbortController | null>(null);

121-151: Toggle loading state for metrics fetcher.

loadingMetrics is never set, so the spinner never shows. Add set/unset like labels/values.

Apply this diff:

   const fetchMetricsCount = React.useCallback(async () => {
     if (selectedPage !== 'metrics' || prometheusStatus !== 'connected') return;

-    try {
+    setLoadingMetrics(true);
+    try {
       const response = await fetch(`${prometheusUrl}/api/v1/label/__name__/values`, {
         method: 'GET',
         signal: AbortSignal.timeout(10000), // 10 second timeout
       });
@@
     } catch (error) {
       console.error('Error fetching metrics:', error);
       setAvailableMetrics([]);
-    }
+    } finally {
+      setLoadingMetrics(false);
+    }
   }, [prometheusUrl, selectedPage, prometheusStatus]);

92-119: Toggle loading state for indices fetcher.

loadingIndices is never set; enable it for better UX feedback.

Apply this diff:

   const fetchIndicesCount = React.useCallback(async () => {
     if (selectedPage !== 'logs' || opensearchStatus !== 'connected') return;

-    try {
+    setLoadingIndices(true);
+    try {
       const response = await fetch(`${opensearchUrl}/_cat/indices?format=json&h=index`, {
         method: 'GET',
         headers: getOpensearchHeaders(),
         signal: AbortSignal.timeout(10000), // 10 second timeout
       });
@@
     } catch (error) {
       console.error('Error fetching indices count:', error);
       setAvailableIndices([]);
-    }
+    } finally {
+      setLoadingIndices(false);
+    }
   }, [opensearchUrl, selectedPage, opensearchStatus, getOpensearchHeaders]);

257-259: Use DOM-safe timeout types in React.

NodeJS.Timeout can cause TS friction in browsers. Prefer ReturnType<typeof setTimeout>.

Apply this diff:

-  const prometheusRetryTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
-  const opensearchRetryTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
+  const prometheusRetryTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
+  const opensearchRetryTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
@@
-    let safetyTimeout: NodeJS.Timeout | null = null;
+    let safetyTimeout: ReturnType<typeof setTimeout> | null = null;
@@
-    let interval: NodeJS.Timeout | null = null;
+    let interval: ReturnType<typeof setInterval> | null = null;

Also applies to: 381-381, 515-515


356-367: Include prometheusStatus in callback deps to avoid stale reads.

The guard if (prometheusStatus === 'checking' ...) can use stale value without it in deps.

Apply this diff:

-  const checkPrometheusConnection = React.useCallback(async (isRetry = false, force = false) => {
+  const checkPrometheusConnection = React.useCallback(async (isRetry = false, force = false) => {
@@
-  }, [prometheusUrl, selectedPage]);
+  }, [prometheusUrl, selectedPage, prometheusStatus]);

Also applies to: 435-435

experimental/ag-ui/front-end/src/components/LogsVisualization.tsx (2)

36-46: Support numeric/epoch timestamps too.

PPL rows may return epoch seconds/millis. Add number handling for better UX.

Example:

if (type === 'timestamp') {
  const toDate = (v: any) =>
    typeof v === 'number'
      ? new Date(v > 1e12 ? v : v * 1000)
      : new Date(String(v));
  const date = toDate(value);
  if (!isNaN(date.getTime()) && date.getTime() > 0) {
    return <span className="timestamp-value">{date.toLocaleString()}</span>;
  }
}

198-207: Harden CSV export for quotes/newlines.

Escape quotes and wrap fields containing comma, quote, or newline to avoid malformed CSV.

Apply this diff:

-                    ...reorderedDatarows.map(row => 
-                      row.map(cell => 
-                        typeof cell === 'string' && cell.includes(',') 
-                          ? `"${cell.replace(/"/g, '""')}"`
-                          : String(cell || '')
-                      ).join(',')
-                    )
+                    ...reorderedDatarows.map(row =>
+                      row.map(cell => {
+                        const raw = String(cell ?? '');
+                        const needsWrap = /[",\n\r]/.test(raw);
+                        const escaped = raw.replace(/"/g, '""');
+                        return needsWrap ? `"${escaped}"` : escaped;
+                      }).join(',')
+                    )
experimental/ag-ui/server.py (3)

83-89: CORS: avoid * with allow_credentials=True.

Browsers block Access-Control-Allow-Credentials: true with wildcard origins. Either set allow_credentials=False or specify explicit origins from config/env.

Example:

allowed = os.getenv("AGUI_CORS_ORIGINS", "").split(",") if os.getenv("AGUI_CORS_ORIGINS") else ["http://localhost:3000"]
app.add_middleware(
    CORSMiddleware,
    allow_origins=allowed,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

398-401: Return a proper JSON list for models.

Avoid double-encoding with json.dumps.

Apply this diff:

-@app.get("/api/model")
-def get_model():
-    return {"model_name": json.dumps(config.get_models_list())}
+@app.get("/api/model")
+def get_model():
+    return {"model_names": config.get_models_list()}

197-209: Preserve exception context on HTTP errors.

Use raise ... from e and prefer conversion flags.

Apply this diff:

-            yield encoder.encode(
+            yield encoder.encode(
                 RunErrorEvent(
                     type=EventType.RUN_ERROR,
-                    message=f"Agent encountered an error: {str(e)}"
+                    message=f"Agent encountered an error: {e!s}"
                 )
             )
             if isinstance(e, AuthenticationError):
-                raise HTTPException(status_code=401, detail=e.message)
+                raise HTTPException(status_code=401, detail=e.message) from e
             elif isinstance(e, litellm.exceptions.RateLimitError):
-                raise HTTPException(status_code=429, detail=e.message)
+                raise HTTPException(status_code=429, detail=e.message) from e
             else:
-                raise HTTPException(status_code=500, detail=str(e))
+                raise HTTPException(status_code=500, detail=str(e)) from e
experimental/ag-ui/front-end/src/components/ChatAssistant.tsx (1)

433-437: Use AbortController for fetch timeouts (timeout option is ignored by browsers).

fetch doesn’t support a timeout option. Use AbortSignal.timeout or a controller.

Apply this diff:

-      const response = await fetch(modelUrl, {
-        method: 'GET',
-        timeout: 5000
-      } as any);
+      const response = await fetch(modelUrl, {
+        method: 'GET',
+        signal: (AbortSignal as any).timeout ? (AbortSignal as any).timeout(5000) : undefined
+      } as any);
@@
-      const response = await fetch(modelUrl, {
-        method: 'GET',
-        timeout: 5000
-      } as any);
+      const response = await fetch(modelUrl, {
+        method: 'GET',
+        signal: (AbortSignal as any).timeout ? (AbortSignal as any).timeout(5000) : undefined
+      } as any);

Also applies to: 449-453

holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (3)

1-17: Imports/logger cleanup and type hints.

  • Add module logger instead of root logging.
  • Import ToolInvokeContext for typing.
  • Remove unused imports (uuid4, formatters, Task, TaskStatus, toolset_name_for_one_liner).
-import logging
-import os
-from typing import Any, Dict
-from uuid import uuid4
+import logging
+import os
+from typing import Any, Dict
@@
-from holmes.core.todo_tasks_formatter import format_tasks
 from holmes.core.tools import (
     StructuredToolResult,
     StructuredToolResultStatus,
     Tool,
     ToolParameter,
     Toolset,
     ToolsetTag,
+    ToolInvokeContext,
 )
-from holmes.plugins.toolsets.investigator.model import Task, TaskStatus
-from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
+
+logger = logging.getLogger(__name__)

As per coding guidelines


52-58: Ruff hint: use explicit conversion flag and module logger.

  • Prefer logger.exception(...) over logging.exception(...).
  • Replace str(e) with {e!s} (RUF010).
-        except Exception as e:
-            logging.exception(f"error using {self.name} tool")
-            return StructuredToolResult(
+        except Exception as e:
+            logger.exception("error using %s tool", self.name)
+            return StructuredToolResult(
                 status=StructuredToolResultStatus.ERROR,
-                error=f"Failed to process tasks: {str(e)}",
+                error=f"Failed to process query: {e!s}",
                 params=params,
             )

Based on static analysis hints


60-63: Optional: Improve one-liner clarity.

Consider quoting the query and omitting the unnecessary parentheses.

-        return f"OpenSearchQueryToolset: Query ({query})"
+        return f'OpenSearchQueryAssist: query="{query}"'
holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2 (1)

31-33: Clarity: “CURRENT USE QUERY” → “CURRENT QUERY”.

Minor wording correction.

-IMPORTANT: YOU CAN USE THE CURRENT USE QUERY TO HELP ENHANCE/MODIFY/FIX/SUGGEST VALID QUERY USING THE SAME INDEX PATTERN
+IMPORTANT: YOU CAN USE THE CURRENT QUERY TO HELP ENHANCE/MODIFY/FIX/SUGGEST A VALID QUERY USING THE SAME INDEX PATTERN
experimental/ag-ui/front-end/src/components/GraphVisualization.tsx (2)

75-84: Remove debug logs; they spam console in production.

Drop console.log calls. Optionally gate behind a debug flag.

-    console.log('Metric data for series', index, ':', metric);
@@
-    console.log('Legend layout check:', {
-      seriesCount,
-      maxLabelLength,
-      hasLongLabels,
-      sampleLabels: datasets.slice(0, 3).map(d => d.label)
-    });
@@
-    console.log('Should use vertical legend:', shouldBeVertical);

Also applies to: 224-229, 241-243


151-212: Optional: memoize chart options for perf.

Use useMemo so options aren’t rebuilt every render.

-  const chartOptions: ChartOptions<'line'> = {
+  const chartOptions: ChartOptions<'line'> = React.useMemo(() => ({
     responsive: true,
     maintainAspectRatio: false,
@@
-  };
+  }), []);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a4fe4c and 352a8bb.

⛔ Files ignored due to path filters (3)
  • experimental/ag-ui/front-end/public/holmesgpt-logo.png is excluded by !**/*.png
  • experimental/ag-ui/front-end/resources/holmesgpt-logo.png is excluded by !**/*.png
  • experimental/ag-ui/front-end/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (28)
  • experimental/ag-ui/README.md (1 hunks)
  • experimental/ag-ui/front-end/.env.example (1 hunks)
  • experimental/ag-ui/front-end/.gitignore (1 hunks)
  • experimental/ag-ui/front-end/.nvmrc (1 hunks)
  • experimental/ag-ui/front-end/README.md (1 hunks)
  • experimental/ag-ui/front-end/package.json (1 hunks)
  • experimental/ag-ui/front-end/public/index.html (1 hunks)
  • experimental/ag-ui/front-end/src/App.css (1 hunks)
  • experimental/ag-ui/front-end/src/App.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/ChatAssistant.css (1 hunks)
  • experimental/ag-ui/front-end/src/components/ChatAssistant.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/ErrorBoundary.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/GraphVisualization.css (1 hunks)
  • experimental/ag-ui/front-end/src/components/GraphVisualization.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/LogsVisualization.css (1 hunks)
  • experimental/ag-ui/front-end/src/components/LogsVisualization.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/MainContent.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/index.css (1 hunks)
  • experimental/ag-ui/front-end/src/index.tsx (1 hunks)
  • experimental/ag-ui/front-end/tsconfig.json (1 hunks)
  • experimental/ag-ui/server.py (1 hunks)
  • holmes/config.py (2 hunks)
  • holmes/plugins/toolsets/__init__.py (2 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2 (1 hunks)
  • pyproject.toml (1 hunks)
  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
holmes/plugins/toolsets/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Toolsets must be located as holmes/plugins/toolsets/{name}.yaml or holmes/plugins/toolsets/{name}/

Files:

  • holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py
  • holmes/plugins/toolsets/__init__.py
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py
  • holmes/plugins/toolsets/__init__.py
  • experimental/ag-ui/server.py
  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
  • holmes/config.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Only use pytest markers that are defined in pyproject.toml; never introduce undefined markers/tags

Files:

  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Test files should mirror the source structure under tests/

Files:

  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
🧬 Code graph analysis (6)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (4)
holmes/core/todo_tasks_formatter.py (1)
  • format_tasks (6-51)
holmes/core/tools.py (7)
  • StructuredToolResult (79-103)
  • StructuredToolResultStatus (52-76)
  • Tool (173-365)
  • ToolParameter (155-161)
  • Toolset (534-768)
  • ToolsetTag (143-146)
  • _load_llm_instructions (763-768)
holmes/plugins/toolsets/investigator/model.py (2)
  • Task (12-15)
  • TaskStatus (6-9)
holmes/plugins/toolsets/utils.py (1)
  • toolset_name_for_one_liner (232-236)
holmes/plugins/toolsets/__init__.py (1)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (1)
  • OpenSearchQueryAssistToolset (65-86)
experimental/ag-ui/front-end/src/components/MainContent.tsx (1)
experimental/ag-ui/front-end/src/App.tsx (1)
  • ObservabilityPage (7-7)
experimental/ag-ui/server.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (20-22)
  • StreamEvents (11-17)
holmes/config.py (5)
  • Config (46-505)
  • load_from_env (168-201)
  • dal (117-120)
  • create_agui_toolcalling_llm (293-304)
  • get_models_list (501-505)
holmes/core/conversations.py (1)
  • build_chat_messages (306-398)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (867-1109)
tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (2)
holmes/core/tools.py (5)
  • StructuredToolResult (79-103)
  • StructuredToolResultStatus (52-76)
  • ToolParameter (155-161)
  • ToolsetTag (143-146)
  • Toolset (534-768)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (6)
  • PplQueryAssistTool (19-62)
  • OpenSearchQueryAssistToolset (65-86)
  • _invoke (38-58)
  • get_parameterized_one_liner (60-62)
  • get_example_config (79-80)
  • _reload_instructions (82-86)
holmes/config.py (3)
holmes/core/tools_utils/tool_executor.py (1)
  • ToolExecutor (17-76)
holmes/core/toolset_manager.py (1)
  • list_console_toolsets (317-332)
holmes/core/tool_calling_llm.py (1)
  • ToolCallingLLM (260-1109)
🪛 Biome (2.1.2)
experimental/ag-ui/front-end/src/App.css

[error] 596-596: Expected a qualified rule, or an at rule but instead found '/'.

Expected a qualified rule, or an at rule here.

(parse)


[error] 597-597: expected , but instead found /

Remove /

(parse)


[error] 1197-1197: Expected a qualified rule, or an at rule but instead found '/'.

Expected a qualified rule, or an at rule here.

(parse)


[error] 1198-1198: expected , but instead found /

Remove /

(parse)


[error] 1408-1408: Expected a qualified rule, or an at rule but instead found '/'.

Expected a qualified rule, or an at rule here.

(parse)


[error] 1409-1409: expected , but instead found /

Remove /

(parse)


[error] 597-597: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 597-597: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 597-597: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 597-597: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 1198-1198: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 1198-1198: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 1198-1198: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 1409-1409: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 1409-1409: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 1409-1409: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 1409-1409: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)


[error] 1409-1409: Unknown type selector is not allowed.

See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.

(lint/correctness/noUnknownTypeSelector)

🪛 dotenv-linter (3.3.0)
experimental/ag-ui/front-end/.env.example

[warning] 4-4: [UnorderedKey] The HOLMES_PORT key should go before the PORT key

(UnorderedKey)


[warning] 12-12: [UnorderedKey] The REACT_APP_OPENSEARCH_PASSWORD key should go before the REACT_APP_OPENSEARCH_URL key

(UnorderedKey)

🪛 Ruff (0.13.3)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py

22-36: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


39-39: Unused method argument: user_approved

(ARG002)


56-56: Use explicit conversion flag

Replace with conversion flag

(RUF010)

experimental/ag-ui/server.py

1-1: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


93-93: Unused function argument: request

(ARG001)


98-98: Redefinition of unused agui_chat from line 93

(F811)


201-201: Use explicit conversion flag

Replace with conversion flag

(RUF010)


205-205: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


207-207: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


209-209: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


277-277: f-string without any placeholders

Remove extraneous f prefix

(F541)


283-283: Local variable title is assigned to but never used

Remove assignment to unused variable title

(F841)


294-299: Consider moving this statement to an else block

(TRY300)

tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py

268-268: Local variable current_file is assigned to but never used

Remove assignment to unused variable current_file

(F841)

🔇 Additional comments (5)
holmes/plugins/toolsets/__init__.py (2)

47-47: LGTM!

The import follows the established pattern for other toolset imports and is correctly placed at the module level as required by the coding guidelines.


97-97: LGTM!

The toolset is correctly instantiated and added to the list, following the same pattern as other OpenSearch-related toolsets.

experimental/ag-ui/front-end/package.json (1)

6-7: No action needed: @Ag-ui packages are published on npm. Both @ag-ui/client@0.0.38 and @ag-ui/core@0.0.38 exist and are accessible.

experimental/ag-ui/front-end/src/App.tsx (1)

38-46: ...

Also applies to: 60-68, 101-111, 123-131

experimental/ag-ui/front-end/src/components/ErrorBoundary.tsx (1)

13-50: Solid error boundary implementation.

State handling, fallback UI, and reset flow look good.

Comment thread experimental/ag-ui/front-end/src/App.css Outdated
Comment thread experimental/ag-ui/front-end/src/components/GraphVisualization.css Outdated
Comment thread experimental/ag-ui/front-end/src/components/MainContent.tsx Outdated
Comment thread experimental/ag-ui/server-agui.py
Comment thread experimental/ag-ui/server-agui.py
Comment thread holmes/plugins/toolsets/opensearch/opensearch_query_assist.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_query_assist.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_query_assist.py Outdated
Comment thread tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
experimental/ag-ui/README.md (5)

9-14: Fix heading level sequencing.

### Known Limitations jumps straight from the H1 title to an H3, tripping MD001 and making the outline harder to scan. Promote it to an H2 so heading levels increase one at a time.


20-21: Adjust nested list indentation.

The sub-bullet under “Data Sources” is indented three spaces, so markdownlint flags MD007. Remove one space (two-space indent total) to keep consistent list formatting.


25-29: Specify language for the shell snippet.

The fenced block lacks a language tag, triggering MD040. Add bash (or shell) after the opening backticks so the commands render with syntax highlighting.


44-55: Add language annotation to the .env example.

This code fence also needs a language identifier for MD040 compliance. Use something like ini to highlight the key/value pairs.


56-59: Tag the frontend command block with a language.

Same MD040 issue here—add bash to the opening backticks for the npm commands.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 352a8bb and b382b4d.

📒 Files selected for processing (1)
  • experimental/ag-ui/README.md (1 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
experimental/ag-ui/README.md

9-9: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


20-20: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


25-25: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


44-44: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (4)
experimental/ag-ui/server.py (4)

1-15: Past issues remain unresolved: import order and env var name.

The previous review identified critical issues in the certificate initialization block that have not been addressed:

  1. Line 1: Unused noqa directive should be removed
  2. Line 9: Environment variable name mismatch (CERTIFICATE vs. ADDITIONAL_CERTIFICATE)
  3. Lines 4-5: Networked imports before certificate setup can initialize clients that don't trust the custom certificate

92-94: Past issue remains unresolved: function name collision.

The health endpoint defines a function named agui_chat which conflicts with the POST endpoint handler on line 98. This causes F811 (function redefinition) and the request parameter is unused.


187-191: Past issue remains unresolved: f-string syntax error.

The f-string on line 189 has nested double quotes causing a syntax error, and the code unsafely assumes the result structure without type checking.


276-306: Past issues remain unresolved: timeseries parsing and unused variable.

The previous review identified issues in _parse_timeseries_data that have not been addressed:

  1. Line 280: Assumes result_data["data"] is a JSON string without checking type
  2. Lines 284, 290: title variable is computed but never used in the return statement (line 302 uses description instead)
  3. Line 284: Unnecessary f-string prefix
🧹 Nitpick comments (4)
experimental/ag-ui/README.md (1)

9-9: Optional: Improve markdown formatting for consistency.

The markdown linter suggests a few formatting improvements:

  • Line 9: Use ## instead of ### (heading levels should increment by one)
  • Line 20: Adjust list indentation to 2 spaces
  • Lines 25 and 47: Add language identifiers to code blocks (e.g., ````bash`)

Also applies to: 20-20, 25-29, 47-58

experimental/ag-ui/server.py (3)

197-210: Improve exception handling: add cause chain and explicit conversion.

The exception handling lacks proper chaining, making debugging harder. Also, line 202 should use an explicit conversion flag.

Apply this diff:

         except Exception as e:
             logging.error(f"Error in /api/agui/chat: {e}", exc_info=True)
             yield encoder.encode(
                 RunErrorEvent(
                     type=EventType.RUN_ERROR,
-                    message=f"Agent encountered an error: {str(e)}"
+                    message=f"Agent encountered an error: {e!s}"
                 )
             )
             if isinstance(e, AuthenticationError):
-                raise HTTPException(status_code=401, detail=e.message)
+                raise HTTPException(status_code=401, detail=e.message) from e
             elif isinstance(e, litellm.exceptions.RateLimitError):
-                raise HTTPException(status_code=429, detail=e.message)
+                raise HTTPException(status_code=429, detail=e.message) from e
             else:
-                raise HTTPException(status_code=500, detail=str(e))
+                raise HTTPException(status_code=500, detail=str(e)) from e

228-235: Add None check for robustness.

_should_execute_suggested_query iterates over frontend_tools without checking if it's None, which could raise a TypeError.

Apply this diff:

 def _should_execute_suggested_query(backend_tool_name: str, frontend_tools: list) -> bool:
+    if not frontend_tools:
+        return False
     for fe_tool_name in frontend_tools:
         if "execute_prometheus" in fe_tool_name and backend_tool_name in (
                 "execute_prometheus_range_query", "execute_prometheus_instant_query"):

170-186: Consider configuration-driven tool mapping.

The backend-to-frontend tool name mapping is hard-coded with string literals. If backend tool names evolve or new query tools are added, this code will require manual updates.

Consider externalizing this mapping to a configuration dict or registry:

BACKEND_TO_FRONTEND_TOOL_MAP = {
    "opensearch_ppl_query_assist": "execute_ppl_query",
    "execute_prometheus_range_query": "execute_promql_query",
    "execute_prometheus_instant_query": "execute_promql_query",
}

# Then use:
front_end_query_tool = BACKEND_TO_FRONTEND_TOOL_MAP.get(tool_name)
if front_end_query_tool:
    # invoke tool

This aligns with the TODO on line 164 about automating front-end tool discovery.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b382b4d and a5797d7.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • experimental/ag-ui/README.md (1 hunks)
  • experimental/ag-ui/server.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server.py
🧬 Code graph analysis (1)
experimental/ag-ui/server.py (5)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (20-22)
  • StreamEvents (11-17)
holmes/config.py (3)
  • Config (46-505)
  • dal (117-120)
  • create_agui_toolcalling_llm (293-304)
holmes/core/conversations.py (1)
  • build_chat_messages (306-398)
holmes/core/tool_calling_llm.py (1)
  • call_stream (867-1109)
🪛 markdownlint-cli2 (0.18.1)
experimental/ag-ui/README.md

9-9: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


20-20: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


25-25: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


47-47: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.13.3)
experimental/ag-ui/server.py

1-1: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


93-93: Unused function argument: request

(ARG001)


98-98: Redefinition of unused agui_chat from line 93

(F811)


202-202: Use explicit conversion flag

Replace with conversion flag

(RUF010)


206-206: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


208-208: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


210-210: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


284-284: f-string without any placeholders

Remove extraneous f prefix

(F541)


290-290: Local variable title is assigned to but never used

Remove assignment to unused variable title

(F841)


301-306: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (1)
experimental/ag-ui/server.py (1)

83-89: Verify CORS configuration for production readiness.

The CORS middleware allows all origins ("*") with credentials enabled. While this may be acceptable for local development and experimental features, it poses security risks if deployed beyond localhost.

Confirm this is intentionally permissive for the experimental AG-UI demonstration, or consider restricting allow_origins to specific domains if this module will be exposed to broader networks.

Comment thread experimental/ag-ui/server.py Outdated
Comment thread experimental/ag-ui/server.py Outdated
Comment thread experimental/ag-ui/server-agui.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (9)
experimental/ag-ui/server.py (8)

5-8: Fix environment variable name mismatch.

The code reads os.environ.get("CERTIFICATE") but should use ADDITIONAL_CERTIFICATE to match the variable name and documentation conventions.

Apply this diff:

-ADDITIONAL_CERTIFICATE: str = os.environ.get("CERTIFICATE", "")
+ADDITIONAL_CERTIFICATE: str = os.environ.get("ADDITIONAL_CERTIFICATE", "")

16-16: Remove unused import.

update_holmes_status_in_db is imported but never used in this module.

Apply this diff:

-from holmes.utils.holmes_status import update_holmes_status_in_db
 import logging

36-36: Remove unused import.

holmes_sync_toolsets_status is imported but never used in this module.

Apply this diff:

 from holmes.core.models import (
     ChatRequest,
 )
-from holmes.utils.holmes_sync_toolsets import holmes_sync_toolsets_status
 from fastapi.middleware.cors import CORSMiddleware

93-96: Remove unused parameter.

The request parameter is declared but never used in the health endpoint.

Apply this diff:

 @app.get("/api/agui/chat/health")
-def agui_chat_health(request: Request):
+def agui_chat_health():
     return JSONResponse(content="ok")

188-192: Fix f-string quoting and handle non-string data safely.

The f-string at line 190 contains nested quotes that break syntax, and the code assumes result['data'] is a string without validation.

Apply this diff:

                         if not front_end_tool_invoked:
+                            # Build a safe snippet from result->data
+                            result_obj = chunk.data.get('result', {})
+                            data_field = result_obj.get('data', '')
+                            if not isinstance(data_field, str):
+                                try:
+                                    data_field = json.dumps(data_field)
+                                except Exception:
+                                    data_field = str(data_field)
+                            snippet = data_field[:200]
                             async for event in _stream_agui_text_message_event(
-                                message=f"🔧 {tool_name} result:\n{chunk.data.get('result', {}).get('data', '')[0:200]}..."
+                                    message=f"🔧 {tool_name} result:\n{snippet}..."):
-                            ):
                                 yield encoder.encode(event)

220-227: Remove unused function.

_remove_empty_user_messages is defined but never invoked anywhere in the module.

If this function should sanitize incoming messages, integrate it into _agui_input_to_holmes_chat_request where messages are processed. Otherwise, remove it.


278-308: Improve data parsing robustness and fix unused variable.

The code assumes result_data["data"] is always a JSON string (line 282) but it may already be a dict. Additionally, title is computed at line 286-292 but the return statement uses description instead (line 304).

Apply this diff:

         # Handle different Prometheus response formats
         prometheus_data = result_data
         result_type = "unknown"
         if "data" in result_data:
-            prometheus_data = json.loads(result_data["data"]).get("data")
+            raw = result_data["data"]
+            try:
+                parsed = json.loads(raw) if isinstance(raw, str) else raw
+            except Exception:
+                parsed = {}
+            prometheus_data = parsed.get("data", parsed)
             result_type = prometheus_data.get("resultType", "unknown")

         # Generate a meaningful title
-        title = f"Prometheus Query Results"
+        title = "Prometheus Query Results"
         if query:
             # Truncate long queries for display
             display_query = query if len(query) <= 50 else query[:47] + "..."
             title = f"Prometheus: {display_query}"
         elif tool_name:
             title = f"{tool_name} Results"

         # Prepare metadata
         metadata = {
             "timestamp": int(time.time()),
             "source": "Prometheus",
             "result_type": result_type,
             "description": description,
             "query": query
         }

         return {
-            "title": description,
+            "title": title if title else (description or "Prometheus Query Results"),
             "query": query,
             "data": prometheus_data,
             "metadata": metadata
         }

362-363: Add bounds check to prevent IndexError.

_is_tool_result_message accesses input_data.messages[-1] without verifying the list is non-empty, which will raise an IndexError if called with no messages.

Apply this diff:

 def _is_tool_result_message(input_data: RunAgentInput) -> bool:
-    return input_data.messages[-1].role == "tool"
+    return len(input_data.messages) > 0 and input_data.messages[-1].role == "tool"
holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 (1)

74-79: This issue was previously flagged and remains unaddressed.

The HTTP request example is still malformed:

  • Missing space between HTTP verb and endpoint (PUT_plugins should be PUT _plugins)
  • JSON body concatenated on the same line instead of appearing on separate line(s)

Apply the previously suggested fix:

-PUT_plugins/_query/settings{"transient":{"plugins":{"ppl":{"enabled":"false"}}}}
+PUT _plugins/_query/settings
+{"transient":{"plugins":{"ppl":{"enabled":"false"}}}}
🧹 Nitpick comments (4)
experimental/ag-ui/README.md (2)

29-33: Add language identifier to code block.

Specify bash as the language for proper syntax highlighting.

Apply this diff:

-```
+```bash
 git clone git@github.com:open-telemetry/opentelemetry-demo.git
 cd opentelemetry-demo
 docker compose up -d

---

`55-66`: **Add language identifier to code block.**

Specify the language (e.g., `env` or `bash`) for proper syntax highlighting.

Apply this diff:

```diff
-```
+```env
 # AG-UI Agent Configuration
 HOLMES_PORT=5050
 AGENT_URL=http://localhost:${HOLMES_PORT}
holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 (2)

95-1616: Consider reducing placeholder code duplication.

The generic PPL example source=my_index | where field1 > 10 | fields field1, field2 appears in approximately 100+ snippets throughout this template, making the file unnecessarily large (1616 lines) with limited instructional variety.

While consistent examples across documentation pages may be intentional, consider:

  1. Consolidating redundant snippets into a single comprehensive PPL syntax reference
  2. Providing diverse examples that demonstrate different PPL commands (stats, dedup, join, eval, etc.) rather than repeating the same basic where/fields pattern
  3. Prioritizing the unique, detailed examples (like the subsearch examples at lines 354-381, 1365-1396) that genuinely illustrate advanced PPL features

This would improve the signal-to-noise ratio for LLM context consumption and make maintenance easier.


1-1616: Consider standardizing language identifiers.

The LANGUAGE field uses various identifiers inconsistently:

  • "PPL" (line 9)
  • "ppl" (line 48)
  • "OpenSearch PPL" (line 100)
  • "OpenSearch API" (line 796)
  • "OpenSearch Dashboards" (line 1292)

While this variation may intentionally reflect different contexts (CLI vs API vs Dashboard), standardizing to a minimal set (e.g., "PPL" for queries, "JSON" for API requests, "bash" for shell) would improve consistency and maintainability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5797d7 and caef97f.

📒 Files selected for processing (8)
  • experimental/ag-ui/README.md (1 hunks)
  • experimental/ag-ui/front-end/src/App.css (1 hunks)
  • experimental/ag-ui/front-end/src/components/GraphVisualization.css (1 hunks)
  • experimental/ag-ui/server.py (1 hunks)
  • holmes/config.py (3 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2 (1 hunks)
  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • holmes/config.py
  • experimental/ag-ui/server.py
holmes/plugins/toolsets/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Toolsets must be located as holmes/plugins/toolsets/{name}.yaml or holmes/plugins/toolsets/{name}/

Files:

  • holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2
🧬 Code graph analysis (2)
holmes/config.py (3)
holmes/core/tools_utils/tool_executor.py (1)
  • ToolExecutor (17-76)
holmes/core/toolset_manager.py (1)
  • list_console_toolsets (317-332)
holmes/core/tool_calling_llm.py (1)
  • ToolCallingLLM (260-1109)
experimental/ag-ui/server.py (5)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (20-22)
  • StreamEvents (11-17)
holmes/config.py (5)
  • Config (46-506)
  • load_from_env (169-202)
  • dal (118-121)
  • create_agui_toolcalling_llm (294-305)
  • get_models_list (502-506)
holmes/core/conversations.py (1)
  • build_chat_messages (306-398)
holmes/core/tool_calling_llm.py (1)
  • call_stream (867-1109)
🪛 markdownlint-cli2 (0.18.1)
experimental/ag-ui/README.md

9-9: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


29-29: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


55-55: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.13.3)
experimental/ag-ui/server.py

94-94: Unused function argument: request

(ARG001)


204-204: Use explicit conversion flag

Replace with conversion flag

(RUF010)


208-208: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


210-210: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


212-212: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


286-286: f-string without any placeholders

Remove extraneous f prefix

(F541)


292-292: Local variable title is assigned to but never used

Remove assignment to unused variable title

(F841)


303-308: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (3)
holmes/config.py (3)

98-98: LGTM!

The separate cache field _agui_tool_executor correctly resolves the cache collision issue identified in previous reviews, ensuring AG-UI and server executors are cached independently.


246-261: LGTM!

The AG-UI tool executor factory follows the established pattern from create_console_tool_executor and correctly uses the dedicated cache field. The refresh_status=True parameter ensures toolset status is current for AG-UI endpoints.


294-305: LGTM!

The AG-UI toolcalling LLM factory correctly delegates to create_agui_tool_executor and follows the pattern established by create_console_toolcalling_llm.

Comment thread experimental/ag-ui/front-end/src/App.css
Comment thread experimental/ag-ui/README.md Outdated
Comment thread experimental/ag-ui/server.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2
Comment thread holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (8)
holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 (1)

77-77: Formatting improvement: Consider separating JSON bodies onto new lines for REST examples.

The major issue (missing space between HTTP verb and endpoint) has been addressed. However, for improved readability and adherence to REST API documentation conventions, consider placing JSON request bodies on separate lines in the HTTP examples at lines 77, 261, and 405.

Example for line 77:

-PUT _plugins/_query/settings{"transient":{"plugins":{"ppl":{"enabled":"false"}}}}
+PUT _plugins/_query/settings
+{"transient":{"plugins":{"ppl":{"enabled":"false"}}}}

Also applies to: 261-261, 405-405

holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (2)

38-40: CRITICAL: Fix _invoke signature to match Tool API.

The _invoke signature is incompatible with the Tool API contract. Tool.invoke (see holmes/core/tools.py lines 212-235) calls _invoke(params=..., context=...), but this implementation accepts user_approved instead, which will raise a TypeError at runtime.

Apply this diff:

+from holmes.core.tools import (
+    StructuredToolResult,
+    StructuredToolResultStatus,
+    Tool,
+    ToolInvokeContext,
+    ToolParameter,
+    Toolset,
+    ToolsetTag,
+)
+
 class PplQueryAssistTool(Tool):
     # ...
     
     def _invoke(
-            self, params: dict, user_approved: bool = False
+        self, params: dict, context: ToolInvokeContext
     ) -> StructuredToolResult:

As per coding guidelines


22-36: Fix parameter schema inconsistencies and mutable default.

Multiple issues with the parameter definition:

  1. Schema inconsistency: query is declared as type="string" but includes items and nested properties (id, content, status), which are only valid for arrays or objects. For a simple string parameter, remove the items field.

  2. Copy-paste artifacts: The nested properties (id, content, status) match the Task model structure from the investigator toolset, not a PPL query string. This confirms these are leftover from copying another tool.

  3. Mutable default: The parameters dict should use Field(default_factory=...) to avoid sharing mutable defaults across instances (RUF012).

Apply this diff:

+from pydantic import Field
+
 class PplQueryAssistTool(Tool):
     name: str = "opensearch_ppl_query_assist"
     description: str = "Generate valid OpenSearch Piped Processing Language (PPL) queries to suggest to users for execution"
-    parameters: Dict[str, ToolParameter] = {
-        "query": ToolParameter(
-            description="valid OpenSearch Piped Processing Language (PPL) query to suggest to users for execution",
-            type="string",
-            required=True,
-            items=ToolParameter(
-                type="object",
-                properties={
-                    "id": ToolParameter(type="string", required=True),
-                    "content": ToolParameter(type="string", required=True),
-                    "status": ToolParameter(type="string", required=True),
-                },
-            ),
-        ),
-    }
+    parameters: Dict[str, ToolParameter] = Field(
+        default_factory=lambda: {
+            "query": ToolParameter(
+                description="Valid OpenSearch Piped Processing Language (PPL) query to suggest to users for execution",
+                type="string",
+                required=True,
+            ),
+        }
+    )

As per coding guidelines

experimental/ag-ui/server.py (5)

16-16: Remove unused imports.

Lines 16 and 36 import update_holmes_status_in_db and holmes_sync_toolsets_status but neither is used in this module.

Also applies to: 36-36


188-192: Fix f-string syntax and handle non-string result data.

Line 190 has a syntax error: double quotes inside an f-string that uses double quotes. Also, chunk.data.get('result', {}).get('data', '') may not return a string.

The previous review comment on lines 188-193 provided a detailed fix for this issue.


207-212: Chain exceptions to preserve traceback.

When re-raising exceptions inside an except block, use raise ... from e to maintain the exception chain and traceback information.

This was previously flagged in the review of lines 207-212.


268-289: Robust timeseries parsing and correct title usage.

Line 272 assumes result_data["data"] is a JSON string, but it may already be a dict. Also, the title variable computed at line 277 (wait, I don't see it in current code - let me recheck)... Actually looking at the code, line 285 uses description as title, which is fine. But line 272 could fail if data is already parsed.

The previous review comment on lines 268-289 addresses the JSON parsing issue.


94-94: Remove unused parameter.

The request parameter is not used in this handler.

Apply this diff:

 @app.get("/api/agui/chat/health")
-def agui_chat_health(request: Request):
+def agui_chat_health():
     return JSONResponse(content="ok")
🧹 Nitpick comments (7)
experimental/ag-ui/front-end/src/components/MainContent.tsx (6)

4-4: Import ObservabilityPage type instead of duplicating.

The ObservabilityPage type is defined in App.tsx (line 6 per relevant snippets) and should be imported rather than redeclared here to maintain a single source of truth.

Apply this diff:

 import React, { useState } from 'react';
 import GraphVisualization from './GraphVisualization';
 import LogsVisualization from './LogsVisualization';
-type ObservabilityPage = 'metrics' | 'logs' | 'traces';
+import { ObservabilityPage } from '../App';

42-42: Remove unused abortControllerRef.

The abortControllerRef is declared but never used. The query functions use AbortSignal.timeout() directly instead of leveraging this ref for cancellation.

If you don't plan to implement manual query cancellation, remove this line:

-  const abortControllerRef = React.useRef<AbortController | null>(null);

159-159: Consider making the series limit configurable.

The hardcoded limit=1000 on the Prometheus /series endpoint may be insufficient for large deployments with many label combinations. Consider making this configurable or implementing pagination.


356-435: Simplify connection checking to avoid race conditions.

The checkPrometheusConnection logic is complex with multiple safety mechanisms (concurrent check prevention, safety timeouts, stuck-state monitoring) that suggest underlying race condition concerns. Including prometheusStatus in the dependency array (line 435) while also setting it creates potential for unexpected behavior.

Consider:

  1. Removing prometheusStatus from dependencies and using a ref to track check state
  2. Simplifying the retry logic
  3. Removing the monitoring useEffect (lines 494-503) which is a workaround for complexity

Example refactor using a ref for check state:

const checkInProgressRef = React.useRef(false);

const checkPrometheusConnection = React.useCallback(async (isRetry = false, force = false) => {
  if (selectedPage !== 'metrics') {
    setPrometheusStatus('connected');
    return;
  }

  if (checkInProgressRef.current && !force) {
    return;
  }

  checkInProgressRef.current = true;
  // ... rest of logic
  
  try {
    // ... fetch logic
  } finally {
    checkInProgressRef.current = false;
  }
}, [prometheusUrl, selectedPage]); // Remove prometheusStatus from deps

570-572: Consider making time range and step configurable.

The hardcoded 1-hour time range and 1-minute step may not suit all use cases. Consider making these configurable via props or UI controls to allow users to adjust the query window and granularity.


1168-1232: Extract data type detection logic to reduce duplication.

The data type detection logic (lines 1168-1232) is duplicated in the maximized modal section (lines 1270-1313). This increases maintenance burden and the risk of inconsistencies.

Consider extracting this into a helper function:

const detectVisualizationType = (data: any): 'logs' | 'metrics' | 'unsupported' => {
  if ((data.schema && data.datarows) || 
      (data.data && data.data.schema && data.data.datarows)) {
    return 'logs';
  }
  if ((data.result !== undefined) || 
      (data.data && data.data.result !== undefined)) {
    return 'metrics';
  }
  return 'unsupported';
};

const structureData = (data: any, type: 'logs' | 'metrics', query: string, page: string) => {
  if (data.title) return data;
  
  return {
    title: type === 'logs' ? 'Logs Visualization' : 'Metrics Visualization',
    query,
    data,
    metadata: {
      timestamp: Date.now() / 1000,
      source: type === 'logs' ? 'OpenSearch PPL' : 'Prometheus'
    }
  };
};

// Then in JSX:
const vizType = detectVisualizationType(currentResult.data);
const structuredData = structureData(currentResult.data, vizType, currentResult.query, selectedPage);

{vizType === 'logs' && <LogsVisualization data={structuredData} />}
{vizType === 'metrics' && <GraphVisualization data={structuredData} />}
{vizType === 'unsupported' && <div className="unsupported-data">...</div>}
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (1)

60-62: Consider consistent naming in one-liner output.

The one-liner returns "OpenSearchQueryToolset" but the actual toolset name is "opensearch/query_assist" (line 70). For consistency, consider either:

  1. Using the imported toolset_name_for_one_liner utility (though this would require keeping that import), or
  2. Simplifying to match the pattern used by other toolsets

Example simplified approach:

     def get_parameterized_one_liner(self, params: Dict) -> str:
         query = params.get("query", "")
-        return f"OpenSearchQueryToolset: Query ({query})"
+        return f"PPL Query: {query[:50]}{'...' if len(query) > 50 else ''}"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between caef97f and 48d134f.

📒 Files selected for processing (4)
  • experimental/ag-ui/front-end/src/components/MainContent.tsx (1 hunks)
  • experimental/ag-ui/server.py (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
holmes/plugins/toolsets/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Toolsets must be located as holmes/plugins/toolsets/{name}.yaml or holmes/plugins/toolsets/{name}/

Files:

  • holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server.py
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py
🧬 Code graph analysis (3)
experimental/ag-ui/server.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (20-22)
  • StreamEvents (11-17)
holmes/config.py (5)
  • Config (46-506)
  • load_from_env (169-202)
  • dal (118-121)
  • create_agui_toolcalling_llm (294-305)
  • get_models_list (502-506)
holmes/core/conversations.py (1)
  • build_chat_messages (306-398)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (867-1109)
experimental/ag-ui/front-end/src/components/MainContent.tsx (1)
experimental/ag-ui/front-end/src/App.tsx (1)
  • ObservabilityPage (7-7)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (4)
holmes/core/todo_tasks_formatter.py (1)
  • format_tasks (6-51)
holmes/core/tools.py (7)
  • StructuredToolResult (79-103)
  • StructuredToolResultStatus (52-76)
  • Tool (173-365)
  • ToolParameter (155-161)
  • Toolset (534-768)
  • ToolsetTag (143-146)
  • _load_llm_instructions (763-768)
holmes/plugins/toolsets/investigator/model.py (2)
  • Task (12-15)
  • TaskStatus (6-9)
holmes/plugins/toolsets/utils.py (1)
  • toolset_name_for_one_liner (232-236)
🪛 Ruff (0.13.3)
experimental/ag-ui/server.py

94-94: Unused function argument: request

(ARG001)


204-204: Use explicit conversion flag

Replace with conversion flag

(RUF010)


208-208: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


210-210: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


212-212: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


284-289: Consider moving this statement to an else block

(TRY300)

holmes/plugins/toolsets/opensearch/opensearch_query_assist.py

22-36: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


39-39: Unused method argument: user_approved

(ARG002)


56-56: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🔇 Additional comments (1)
experimental/ag-ui/server.py (1)

343-344: LGTM! Bounds check added.

The function now properly checks for non-empty messages before accessing the last element, preventing potential IndexError.

Comment thread experimental/ag-ui/front-end/src/components/MainContent.tsx
Comment thread experimental/ag-ui/server.py Outdated
Comment thread experimental/ag-ui/server-agui.py Outdated
Comment thread experimental/ag-ui/server.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_query_assist.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_query_assist.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
experimental/ag-ui/server.py (4)

16-16: Remove unused imports (previously flagged).

Lines 16 and 36 import update_holmes_status_in_db and holmes_sync_toolsets_status respectively, but neither is used in this module.

Also applies to: 36-36


222-223: Fix PromQL tool name matching (previously flagged).

Line 222 checks for "execute_prometheus" but the frontend uses "execute_promql_query". This mismatch prevents the frontend tool from being invoked.

Apply this diff:

-        if "execute_prometheus" in fe_tool_name and backend_tool_name in (
+        if "execute_promql" in fe_tool_name and backend_tool_name in (
                 "execute_prometheus_range_query", "execute_prometheus_instant_query"):
             return True

268-273: Add safe JSON parsing for timeseries data (previously flagged).

Line 272 assumes result_data["data"] is a JSON string without checking. It may already be a dict, causing json.loads() to fail.

Apply this diff:

         prometheus_data = result_data
         result_type = "unknown"
         if "data" in result_data:
-            prometheus_data = json.loads(result_data["data"]).get("data")
+            raw = result_data["data"]
+            try:
+                parsed = json.loads(raw) if isinstance(raw, str) else raw
+            except Exception:
+                parsed = {}
+            prometheus_data = parsed.get("data", parsed)
             result_type = prometheus_data.get("resultType", "unknown")

357-359: Avoid mutating input messages (previously flagged).

Lines 357-359 assign msg_tmp = msg then modify msg_tmp.role, which mutates the original message in input_data.messages.

Apply this diff:

         elif msg.role == "tool":
-            msg_tmp = msg
-            msg_tmp.role = "assistant"
-            non_system_messages.append(msg_tmp)
+            from copy import copy
+            msg_copy = copy(msg)
+            msg_copy.role = "assistant"
+            non_system_messages.append(msg_copy)
🧹 Nitpick comments (3)
experimental/ag-ui/server.py (3)

94-94: Remove unused request parameter.

The request parameter is not used in the health check handler.

Apply this diff:

 @app.get("/api/agui/chat/health")
-def agui_chat_health(request: Request):
+def agui_chat_health():
     return JSONResponse(content="ok")

59-59: Add type hints for better type safety.

Several functions lack complete type hints, which are required per coding guidelines for mypy compliance:

  • Line 59: init_logging() missing return type -> None
  • Line 126: event_generator(message_history) missing param type message_history: list and return type
  • Line 308: _invoke_front_end_tool(..., tool_call_args: dict) should specify dict contents or use Dict[str, Any], missing return type
  • Line 325: _stream_agui_text_message_event(...) missing return type

Example improvements:

def init_logging() -> None:
    ...

async def event_generator(message_history: list) -> AsyncGenerator[bytes, None]:
    ...

async def _invoke_front_end_tool(
    tool_call_id: str, 
    tool_call_name: str, 
    tool_call_args: Dict[str, Any]
) -> AsyncGenerator[Union[ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent], None]:
    ...

async def _stream_agui_text_message_event(
    message: str
) -> AsyncGenerator[Union[TextMessageStartEvent, TextMessageContentEvent, TextMessageEndEvent], None]:
    ...

Also applies to: 126-126, 308-308, 325-325


84-90: Consider restricting CORS origins for production use.

The current CORS configuration allows all origins (allow_origins=["*"]), which is acceptable for experimental/demo purposes but should be tightened before production deployment.

For production, specify allowed origins explicitly:

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://your-frontend-domain.com"],  # Restrict to known origins
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Or read from configuration:

allowed_origins = os.environ.get("ALLOWED_ORIGINS", "*").split(",")
app.add_middleware(
    CORSMiddleware,
    allow_origins=allowed_origins,
    ...
)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48d134f and c961cdd.

📒 Files selected for processing (1)
  • experimental/ag-ui/server.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server.py
🧬 Code graph analysis (1)
experimental/ag-ui/server.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (20-22)
  • StreamEvents (11-17)
holmes/config.py (5)
  • Config (46-506)
  • load_from_env (169-202)
  • dal (118-121)
  • create_agui_toolcalling_llm (294-305)
  • get_models_list (502-506)
holmes/core/conversations.py (1)
  • build_chat_messages (306-398)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (867-1109)
🪛 Ruff (0.13.3)
experimental/ag-ui/server.py

94-94: Unused function argument: request

(ARG001)


204-204: Use explicit conversion flag

Replace with conversion flag

(RUF010)


284-289: Consider moving this statement to an else block

(TRY300)

Comment thread experimental/ag-ui/server.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
experimental/ag-ui/server.py (4)

214-221: Fix frontend tool name matching.

Line 216 checks for "execute_prometheus" but the frontend uses "execute_promql_query". This mismatch prevents proper query execution detection.

Apply this diff:

 def _should_execute_suggested_query(backend_tool_name: str, frontend_tools: list) -> bool:
     for fe_tool_name in frontend_tools:
-        if "execute_prometheus" in fe_tool_name and backend_tool_name in (
+        if "execute_promql" in fe_tool_name and backend_tool_name in (
                 "execute_prometheus_range_query", "execute_prometheus_instant_query"):
             return True
         elif "execute_ppl" in fe_tool_name and backend_tool_name == "execute_ppl_query":
             return True
     return False

Based on past review comments


347-353: Avoid mutating original input messages.

Lines 351-353 assign msg_tmp = msg then modify msg_tmp.role, which mutates the original message object in input_data.messages. This creates unexpected side effects.

Apply this diff:

         elif msg.role == "tool":
-            msg_tmp = msg
-            msg_tmp.role = "assistant"
-            non_system_messages.append(msg_tmp)
+            from copy import copy
+            msg_copy = copy(msg)
+            msg_copy.role = "assistant"
+            non_system_messages.append(msg_copy)

Based on past review comments


262-283: Add robust JSON parsing for timeseries data.

Line 266 assumes result_data["data"] is always a JSON string and calls json.loads() unconditionally. If it's already a dict or other type, this will raise an exception.

Apply this diff:

         # Handle different Prometheus response formats
         prometheus_data = result_data
         result_type = "unknown"
         if "data" in result_data:
-            prometheus_data = json.loads(result_data["data"]).get("data")
-            result_type = prometheus_data.get("resultType", "unknown")
+            raw = result_data["data"]
+            try:
+                parsed = json.loads(raw) if isinstance(raw, str) else raw
+            except Exception:
+                parsed = {}
+            prometheus_data = parsed.get("data", parsed)
+            result_type = prometheus_data.get("resultType", "unknown")

Based on past review comments


188-192: Add type safety for data slicing.

The code assumes chunk.data.get('result', {}).get('data', '') returns a string or sliceable type. If data is a dict, list, or other non-string type, the slice operation [0:200] may fail or produce unexpected results.

Apply this diff:

                         if not front_end_tool_invoked:
+                            result_obj = chunk.data.get('result', {})
+                            data_field = result_obj.get('data', '')
+                            if not isinstance(data_field, str):
+                                try:
+                                    data_field = json.dumps(data_field)
+                                except Exception:
+                                    data_field = str(data_field)
+                            snippet = data_field[:200]
                             async for event in _stream_agui_text_message_event(
-                                message=f"🔧 {tool_name} result:\n{chunk.data.get('result', {}).get('data', '')[0:200]}..."
+                                message=f"🔧 {tool_name} result:\n{snippet}..."
                             ):
                                 yield encoder.encode(event)

Based on past review comments

🧹 Nitpick comments (5)
experimental/ag-ui/README.md (2)

29-33: Add language identifier to code block.

The fenced code block is missing a language specifier, which prevents proper syntax highlighting and may affect documentation rendering tools.

Apply this diff:

-```
+```bash
 git clone git@github.com:open-telemetry/opentelemetry-demo.git
 cd opentelemetry-demo
 docker compose up -d

---

`55-66`: **Add language identifier to code block.**

The environment configuration code block is missing a language specifier. Use `bash` or `env` to enable syntax highlighting.



Apply this diff:

```diff
-```
+```bash
 # AG-UI Agent Configuration
 HOLMES_PORT=5050
 AGENT_URL=http://localhost:${HOLMES_PORT}
 # Prometheus Configuration
 REACT_APP_PROMETHEUS_URL=http://localhost:9090
 
 # OpenSearch Configuration
 REACT_APP_OPENSEARCH_URL=http://localhost:9200
 REACT_APP_OPENSEARCH_USER=user
 REACT_APP_OPENSEARCH_PASSWORD=pass

</blockquote></details>
<details>
<summary>experimental/ag-ui/server.py (3)</summary><blockquote>

`16-16`: **Remove unused import.**

`update_holmes_status_in_db` is imported but never used in this module.



Apply this diff:

```diff
-from holmes.utils.holmes_status import update_holmes_status_in_db
 import logging

Based on past review comments


36-36: Remove unused import.

holmes_sync_toolsets_status is imported but never used in this module.

Apply this diff:

-from holmes.utils.holmes_sync_toolsets import holmes_sync_toolsets_status
 from fastapi.middleware.cors import CORSMiddleware

Based on past review comments


93-95: Remove unused parameter.

The request parameter is not used in the health check handler.

Apply this diff:

 @app.get("/api/agui/chat/health")
-def agui_chat_health(request: Request):
+def agui_chat_health():
     return JSONResponse(content="ok")

Based on past review comments

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c961cdd and 0e9d5e2.

📒 Files selected for processing (4)
  • experimental/ag-ui/README.md (1 hunks)
  • experimental/ag-ui/server.py (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (1 hunks)
  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server.py
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py
holmes/plugins/toolsets/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Toolsets must be located as holmes/plugins/toolsets/{name}.yaml or holmes/plugins/toolsets/{name}/

Files:

  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py
🧬 Code graph analysis (2)
experimental/ag-ui/server.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (20-22)
  • StreamEvents (11-17)
holmes/config.py (5)
  • Config (46-506)
  • load_from_env (169-202)
  • dal (118-121)
  • create_agui_toolcalling_llm (294-305)
  • get_models_list (502-506)
holmes/core/conversations.py (1)
  • build_chat_messages (306-398)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (867-1109)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (1)
holmes/core/tools.py (8)
  • StructuredToolResult (79-103)
  • StructuredToolResultStatus (52-76)
  • Tool (173-365)
  • ToolParameter (155-161)
  • Toolset (534-768)
  • ToolsetTag (143-146)
  • ToolInvokeContext (164-170)
  • _load_llm_instructions (763-768)
🪛 markdownlint-cli2 (0.18.1)
experimental/ag-ui/README.md

29-29: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


55-55: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.13.3)
experimental/ag-ui/server.py

94-94: Unused function argument: request

(ARG001)


204-204: Use explicit conversion flag

Replace with conversion flag

(RUF010)


278-283: Consider moving this statement to an else block

(TRY300)

holmes/plugins/toolsets/opensearch/opensearch_query_assist.py

32-32: Unused method argument: context

(ARG002)


48-48: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🔇 Additional comments (1)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (1)

1-78: LGTM!

The toolset implementation correctly follows the Tool API and HolmesGPT toolset patterns:

  • Proper import structure and minimal dependencies
  • Correct _invoke signature with context: ToolInvokeContext parameter
  • Appropriate parameter schema (string type for PPL query)
  • Clean error handling with proper status and error messages
  • Template-based instruction loading via _reload_instructions

The unused context parameter (static analysis hint on line 32) is normal for simple tools that don't require context—keeping it maintains API consistency.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (5)
experimental/ag-ui/server.py (5)

89-91: Remove unused request parameter.

The request parameter is not used in this health check handler and should be removed for clarity.

Apply this diff:

 @app.get("/api/agui/chat/health")
-def agui_chat_health(request: Request):
+def agui_chat_health():
     return JSONResponse(content="ok")

210-217: Fix tool name matching for query execution.

Two issues in the tool name matching logic:

  1. Line 212 checks for "execute_prom" but the frontend uses "execute_promql_query" (visible in the frontend code).
  2. Line 215 checks backend_tool_name == "execute_ppl_query" but line 172 shows the actual backend tool name is "opensearch_ppl_query_assist".

Apply this diff:

 def _should_execute_suggested_query(backend_tool_name: str, frontend_tools: list) -> bool:
     for fe_tool_name in frontend_tools:
-        if "execute_prom" in fe_tool_name and backend_tool_name in (
+        if "execute_promql" in fe_tool_name and backend_tool_name in (
                 "execute_prometheus_range_query", "execute_prometheus_instant_query"):
             return True
-        elif "execute_ppl" in fe_tool_name and backend_tool_name == "execute_ppl_query":
+        elif "execute_ppl" in fe_tool_name and backend_tool_name == "opensearch_ppl_query_assist":
             return True
     return False

184-189: Fix f-string syntax error and add type safety.

Line 186 has two critical issues:

  1. The f-string uses inner double quotes which breaks the syntax: f"🔧 {tool_name} result:\n{chunk.data.get("result", {}).get("data", "")[0:200]}..."
  2. It assumes data is a string/list without type checking, which can cause runtime errors if the data is a dict or other type.

Apply this diff:

                         if not front_end_tool_invoked:
+                            # Safely extract and format result data
+                            result_obj = chunk.data.get('result', {}) if hasattr(chunk, 'data') else {}
+                            data_field = result_obj.get('data', '')
+                            if not isinstance(data_field, str):
+                                try:
+                                    data_field = json.dumps(data_field)
+                                except Exception:
+                                    data_field = str(data_field)
+                            snippet = data_field[:200]
                             async for event in _stream_agui_text_message_event(
-                                message=f"🔧 {tool_name} result:\n{chunk.data.get('result', {}).get('data', '')[0:200]}..."
+                                message=f"🔧 {tool_name} result:\n{snippet}..."
                             ):
                                 yield encoder.encode(event)

258-279: Add type safety for timeseries data parsing.

Line 262 calls json.loads(result_data["data"]) without checking if the data is already parsed. If result_data["data"] is already a dict, this will raise a TypeError.

Apply this diff:

         # Handle different Prometheus response formats
         prometheus_data = result_data
         result_type = "unknown"
         if "data" in result_data:
-            prometheus_data = json.loads(result_data["data"]).get("data")
+            raw = result_data["data"]
+            try:
+                parsed = json.loads(raw) if isinstance(raw, str) else raw
+            except Exception:
+                parsed = {}
+            prometheus_data = parsed.get("data", parsed)
             result_type = prometheus_data.get("resultType", "unknown")

343-349: Avoid mutating input messages.

Lines 347-349 assign msg_tmp = msg then modify msg_tmp.role, which mutates the original message object in input_data.messages. This can cause unexpected side effects for the caller.

Apply this diff:

         elif msg.role == "tool":
-            msg_tmp = msg
-            msg_tmp.role = "assistant"
-            non_system_messages.append(msg_tmp)
+            # Create a new message object to avoid mutating the original
+            from copy import copy
+            msg_copy = copy(msg)
+            msg_copy.role = "assistant"
+            non_system_messages.append(msg_copy)
🧹 Nitpick comments (1)
experimental/ag-ui/server.py (1)

195-202: Consider using conversion flag in f-string.

Line 200 uses str(e) inside the f-string. Python's f-string conversion flags provide a more idiomatic way to achieve this.

Apply this diff:

             yield encoder.encode(
                 RunErrorEvent(
                     type=EventType.RUN_ERROR,
-                    message=f"Agent encountered an error: {str(e)}"
+                    message=f"Agent encountered an error: {e!s}"
                 )
             )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e9d5e2 and 6388397.

📒 Files selected for processing (2)
  • experimental/ag-ui/README.md (1 hunks)
  • experimental/ag-ui/server.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server.py
🧬 Code graph analysis (1)
experimental/ag-ui/server.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (20-22)
  • StreamEvents (11-17)
holmes/config.py (5)
  • Config (46-506)
  • load_from_env (169-202)
  • dal (118-121)
  • create_agui_toolcalling_llm (294-305)
  • get_models_list (502-506)
holmes/core/conversations.py (1)
  • build_chat_messages (306-398)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (867-1109)
🪛 Ruff (0.13.3)
experimental/ag-ui/server.py

90-90: Unused function argument: request

(ARG001)


200-200: Use explicit conversion flag

Replace with conversion flag

(RUF010)


274-279: Consider moving this statement to an else block

(TRY300)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
experimental/ag-ui/server.py (1)

346-349: Avoid mutating input message objects.

Assigning msg_tmp = msg and then modifying msg_tmp.role = "assistant" mutates the original message object in input_data.messages, which can cause unexpected side effects.

Apply this diff to create a proper copy:

         elif msg.role == "tool":
-            msg_tmp = msg
-            msg_tmp.role = "assistant"
-            non_system_messages.append(msg_tmp)
+            from copy import copy
+            msg_copy = copy(msg)
+            msg_copy.role = "assistant"
+            non_system_messages.append(msg_copy)

Alternatively, construct a new message object to avoid any shared references.

experimental/ag-ui/front-end/src/components/MainContent.tsx (1)

794-848: Refactor to eliminate code duplication.

The triggered query execution logic (lines 800-843) duplicates the query routing and result handling already implemented in handleExecuteQuery (lines 701-775). This duplication increases maintenance burden and risks inconsistencies between the two code paths.

As noted in past review comments, refactor to reuse the existing query execution logic. Wrap handleExecuteQuery in useCallback so it can be safely called from the effect:

const handleExecuteQuery = React.useCallback(async () => {
  if (!query.trim() || isExecuting) return;
  
  // ... existing implementation ...
}, [query, isExecuting, selectedPage, /* other dependencies */]);

React.useEffect(() => {
  if (triggerQuery && triggerQuery.trim() && triggerQuery !== processedTriggerQuery.current) {
    processedTriggerQuery.current = triggerQuery;
    setQuery(triggerQuery);
    
    setTimeout(() => {
      handleExecuteQuery();
      if (onQueryTriggered) {
        onQueryTriggered();
      }
    }, 100);
  }
}, [triggerQuery, selectedPage, onQueryTriggered, handleExecuteQuery]);
🧹 Nitpick comments (6)
experimental/ag-ui/server.py (4)

90-90: Remove unused request parameter.

The request parameter is declared but never used in the health check handler.

As per static analysis hints

Apply this diff:

 @app.get("/api/agui/chat/health")
-def agui_chat_health(request: Request):
+def agui_chat_health():
     return JSONResponse(content="ok")

186-186: Safely handle non-string result data before slicing.

The code assumes chunk.data.get('result', {}).get('data', '') returns a string or list that can be sliced with [0:200]. If data is a complex object (dict, custom type), this will raise a TypeError.

Apply this diff to safely extract and format the result snippet:

                         if not front_end_tool_invoked:
+                            # Safely extract result data
+                            result_obj = chunk.data.get('result', {})
+                            data_field = result_obj.get('data', '')
+                            if not isinstance(data_field, str):
+                                try:
+                                    data_field = str(data_field)
+                                except Exception:
+                                    data_field = ''
+                            snippet = data_field[:200] if data_field else ''
                             async for event in _stream_agui_text_message_event(
-                                message=f"🔧 {tool_name} result:\n{chunk.data.get('result', {}).get('data', '')[0:200]}..."
+                                message=f"🔧 {tool_name} result:\n{snippet}..."
                             ):
                                 yield encoder.encode(event)

200-200: Use explicit string conversion flag in f-string.

Replace f"{str(e)}" with f"{e!s}" for cleaner, more idiomatic string conversion.

As per static analysis hints

Apply this diff:

             yield encoder.encode(
                 RunErrorEvent(
                     type=EventType.RUN_ERROR,
-                    message=f"Agent encountered an error: {str(e)}"
+                    message=f"Agent encountered an error: {e!s}"
                 )
             )

232-295: Enhance robustness of timeseries data parsing.

At line 262, json.loads(result_data["data"]) assumes result_data["data"] is a JSON string. If it's already a dict (which is common), this will raise a TypeError. The code should check the type before parsing.

Apply this diff to safely handle both string and dict formats:

         # Handle different Prometheus response formats
         prometheus_data = result_data
         result_type = "unknown"
         if "data" in result_data:
-            prometheus_data = json.loads(result_data["data"]).get("data")
+            raw_data = result_data["data"]
+            try:
+                parsed = json.loads(raw_data) if isinstance(raw_data, str) else raw_data
+            except (json.JSONDecodeError, TypeError):
+                parsed = {}
+            prometheus_data = parsed.get("data", parsed) if isinstance(parsed, dict) else {}
             result_type = prometheus_data.get("resultType", "unknown")
experimental/ag-ui/front-end/src/components/MainContent.tsx (2)

29-1334: Consider splitting into smaller components.

This component is 1336 lines and handles multiple concerns: connection management, query execution, data fetching, three different explorers, visualizations, and modal management. Consider extracting smaller, focused components:

  • ConnectionStatusBar (lines 895-943)
  • PrometheusSeriesExplorer (lines 945-1051)
  • OpenSearchIndicesExplorer (lines 1053-1102)
  • QueryInputSection (lines 1104-1151)
  • ResultsVisualization (lines 1153-1257)

This would improve maintainability, testability, and readability.


1179-1327: Consider extracting duplicated visualization rendering logic.

The logic for detecting data type and rendering the appropriate visualization (GraphVisualization vs LogsVisualization) is duplicated between the inline view (lines 1182-1246) and the modal view (lines 1284-1327).

Consider extracting to a helper function:

const renderVisualization = (data: any, query: string) => {
  const isLogsData = (data.schema && data.datarows) || 
                     (data.data && data.data.schema && data.data.datarows);
  const isMetricsData = (data.result !== undefined) || 
                        (data.data && data.data.data.result !== undefined);
  
  if (isLogsData) {
    return (
      <LogsVisualization 
        data={data.title ? data : {
          title: selectedPage === 'logs' ? 'Logs Visualization' : 'Data Visualization',
          query: query,
          data: data,
          metadata: { timestamp: Date.now() / 1000, source: 'OpenSearch PPL' }
        }} 
      />
    );
  } else if (isMetricsData) {
    return (
      <GraphVisualization 
        data={data.title ? data : {
          title: selectedPage === 'metrics' ? 'Metrics Visualization' : 'Data Visualization',
          query: query,
          data: data,
          metadata: { timestamp: Date.now() / 1000, source: 'Prometheus' }
        }}
      />
    );
  } else {
    return (
      <div className="unsupported-data">
        <span className="error-icon">⚠️</span>
        <div className="error-text">Unsupported data format</div>
        <div className="error-details">
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
      </div>
    );
  }
};

Then use {renderVisualization(currentResult.data, currentResult.query)} in both locations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6388397 and 0fcebb8.

📒 Files selected for processing (2)
  • experimental/ag-ui/front-end/src/components/MainContent.tsx (1 hunks)
  • experimental/ag-ui/server.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server.py
🧬 Code graph analysis (2)
experimental/ag-ui/front-end/src/components/MainContent.tsx (1)
experimental/ag-ui/front-end/src/App.tsx (1)
  • ObservabilityPage (7-7)
experimental/ag-ui/server.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (20-22)
  • StreamEvents (11-17)
holmes/config.py (5)
  • Config (46-506)
  • load_from_env (169-202)
  • dal (118-121)
  • create_agui_toolcalling_llm (294-305)
  • get_models_list (502-506)
holmes/core/conversations.py (1)
  • build_chat_messages (306-398)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (867-1109)
🪛 Ruff (0.13.3)
experimental/ag-ui/server.py

90-90: Unused function argument: request

(ARG001)


200-200: Use explicit conversion flag

Replace with conversion flag

(RUF010)


274-279: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (2)
experimental/ag-ui/server.py (1)

210-217: LGTM: Flexible frontend tool name matching.

The substring check "execute_prom" in fe_tool.name appropriately matches both execute_prometheus_* and execute_promql_* frontend tool variants, providing good flexibility for tool name variations.

experimental/ag-ui/front-end/src/components/MainContent.tsx (1)

553-558: Include onQueryUpdate in the effect’s dependency array
onQueryUpdate is stable (wrapped in useCallback([])), and the existing isUpdatingFromParent ref prevents feedback loops.

Likely an incorrect or invalid review comment.

Comment thread experimental/ag-ui/front-end/src/components/MainContent.tsx
Comment thread experimental/ag-ui/front-end/src/components/MainContent.tsx
Comment thread experimental/ag-ui/front-end/src/components/MainContent.tsx
Comment thread experimental/ag-ui/front-end/src/components/MainContent.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (8)
tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (2)

136-150: Remove or rename redundant test.

This test is functionally identical to test_invoke_with_valid_query (lines 39-53) and doesn't verify anything specific about user approval behavior. The test name suggests it should test approval logic, but it simply invokes the tool with default parameters.

Consider either:

  • Removing this test as duplicate coverage, or
  • Renaming it to something like test_invoke_with_default_behavior if you want to keep it for clarity

332-396: LGTM! Solid integration test coverage.

The integration tests effectively verify:

  • Tool and toolset interoperability
  • Configuration consistency across components
  • Template loading behavior with proper filesystem mocking

Optional enhancement: Consider adding a test that triggers the exception handler in PplQueryAssistTool._invoke (lines 47-51 in the source). Currently, all tests result in SUCCESS status. You could add a test that mocks an internal failure to ensure the error handling path works correctly.

experimental/ag-ui/front-end/src/components/ChatAssistant.tsx (6)

230-355: Reduce duplication in query execution handlers.

The execute_promql_query and execute_ppl_query handlers (lines 230-292 and 293-355) have nearly identical logic differing only in the callback and message text.

Extract a common handler:

const handleQueryExecution = (
  toolCallId: string,
  accumulatedArgsString: string,
  queryType: 'PromQL' | 'PPL',
  callback?: (query: string) => void,
  pageName: string
) => {
  try {
    let args: any = {};
    if (accumulatedArgsString) {
      try {
        args = JSON.parse(accumulatedArgsString);
      } catch (parseError) {
        console.warn('Could not parse accumulated args as JSON:', parseError);
        args = {};
      }
    }
    
    console.log(`Execute ${queryType} query parsed args:`, args);
    
    if (args.query && callback) {
      callback(args.query);
      
      const successMessage: ChatMessage = {
        id: 'tool-' + toolCallId,
        text: `✅ Navigated to ${pageName} page and executed query: \`${args.query}\``,
        sender: 'assistant',
        timestamp: new Date()
      };
      
      setMessages(prev => 
        prev.map(msg => 
          msg.id === 'tool-' + toolCallId ? successMessage : msg
        )
      );
    } else {
      const errorDetails = [];
      if (!args.query) errorDetails.push('query parameter');
      if (!callback) errorDetails.push('callback function');
      throw new Error(`Missing: ${errorDetails.join(', ')}. Args: ${JSON.stringify(args)}`);
    }
  } catch (error) {
    console.error(`Error executing ${queryType} query:`, error);
    
    const errorMessage: ChatMessage = {
      id: 'tool-' + toolCallId,
      text: `❌ Failed to execute ${queryType} query: ${error instanceof Error ? error.message : 'Unknown error'}`,
      sender: 'assistant',
      timestamp: new Date()
    };
    
    setMessages(prev => 
      prev.map(msg => 
        msg.id === 'tool-' + toolCallId ? errorMessage : msg
      )
    );
  }
};

Then replace both blocks with:

} else if (toolCallInfo?.name === 'execute_promql_query') {
  handleQueryExecution(
    params.event.toolCallId,
    accumulatedArgsString,
    'PromQL',
    onExecutePromQLQuery,
    'Metrics'
  );
} else if (toolCallInfo?.name === 'execute_ppl_query') {
  handleQueryExecution(
    params.event.toolCallId,
    accumulatedArgsString,
    'PPL',
    onExecutePPLQuery,
    'Logs'
  );
}

68-68: Extract magic numbers to named constants.

Multiple magic numbers throughout the code reduce readability and maintainability. Consider extracting them to named constants.

Add these constants at the top of the component:

const TIMEOUTS = {
  INITIAL_MESSAGE_DELAY: 100,      // Line 68
  CONNECTION_CHECK_DELAY: 500,     // Line 669
  FETCH_TIMEOUT: 5000,             // Used in checkConnection and fetchModel
  RUN_AGENT_TIMEOUT: 120000,       // Line 831
  MAX_RECONNECT_DELAY: 30000,      // Line 592
} as const;

const UI_CONSTRAINTS = {
  MIN_PANEL_WIDTH: 250,            // Line 691
  MAX_PANEL_WIDTH_RATIO: 0.8,      // Line 692
  DEFAULT_PANEL_WIDTH: 500,        // Line 39
} as const;

Then replace the numeric literals with the named constants throughout the code.

Also applies to: 592-592, 669-669, 691-692, 831-831


64-64: Consider conditional logging for production.

Extensive console logging throughout the component may expose sensitive information in production and adds noise.

Wrap debug logs in a conditional or create a debug utility:

const DEBUG = process.env.NODE_ENV === 'development';

const debugLog = (...args: any[]) => {
  if (DEBUG) {
    console.log(...args);
  }
};

const debugError = (...args: any[]) => {
  if (DEBUG) {
    console.error(...args);
  }
};

Then replace:

  • console.log(...) with debugLog(...)
  • Keep console.error(...) for actual errors, or use debugError(...) for debug-only errors

Also applies to: 79-79, 96-96, 103-103, 133-133, 171-171, 179-179, 185-185, 199-199, 243-243, 306-306


14-14: Improve type safety for better error detection.

Several uses of loose types (any, Object) reduce TypeScript's ability to catch errors.

Consider defining proper types:

interface GraphData {
  title: string;
  data: {
    result?: Array<{
      metric: Record<string, string>;
      values: Array<[number, string]>;
    }>;
  };
  query?: string;
  metadata?: Record<string, any>;
}

interface ChatMessage {
  id: string;
  text?: string;
  sender: 'user' | 'assistant';
  timestamp: Date;
  type?: 'text' | 'graph' | 'error';
  graphData?: GraphData;  // Instead of 'any'
  error?: {
    title: string;
    description: string;
    retryable?: boolean;
  };
}

// For the subscriber, use proper AG-UI types if available from the package
interface AgentEventSubscriber {
  onRunStartedEvent?: (params: { event: any }) => void;
  onRunFinishedEvent?: (params: { event: any }) => void;
  // ... other event handlers
}

const subscriber: AgentEventSubscriber = {
  // ...
};

Also applies to: 90-90


753-765: Consider input validation for robustness.

User input is sent directly to the backend with only whitespace checks. While this may be acceptable for an internal observability tool, consider whether additional validation is needed based on your security requirements.

If the backend doesn't have input validation, you might want to add:

  • Maximum length checks
  • Character allowlist/blocklist
  • Sanitization of special characters if they could cause issues with the backend

Example:

const MAX_MESSAGE_LENGTH = 10000;

const sanitizeInput = (input: string): string => {
  return input.trim().slice(0, MAX_MESSAGE_LENGTH);
};

const messageContent = sanitizeInput(userMessage.text || '');
if (messageContent.length === 0) {
  throw new Error('Cannot send empty message');
}

35-1045: Consider component decomposition for maintainability.

This component handles multiple responsibilities: agent lifecycle management, connection management, message handling, UI rendering, and error recovery. For long-term maintainability, consider splitting into smaller components or hooks.

Potential structure:

  • useAgentConnection hook: agent initialization, connection status, reconnection logic
  • useMessageHandler hook: message sending, receiving, tool call handling
  • ChatMessages component: message rendering logic
  • ChatInput component: input field, history navigation
  • ChatAssistant component: orchestrates the above

This would make each piece easier to test and maintain, though it may be premature optimization for an experimental feature.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fcebb8 and 84345e0.

📒 Files selected for processing (2)
  • experimental/ag-ui/front-end/src/components/ChatAssistant.tsx (1 hunks)
  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Only use pytest markers that are defined in pyproject.toml; never introduce undefined markers/tags

Files:

  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Test files should mirror the source structure under tests/

Files:

  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
🧬 Code graph analysis (1)
tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (2)
holmes/core/tools.py (6)
  • StructuredToolResult (79-103)
  • StructuredToolResultStatus (52-76)
  • ToolParameter (155-161)
  • ToolsetTag (143-146)
  • ToolInvokeContext (164-170)
  • Toolset (534-768)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (6)
  • PplQueryAssistTool (16-54)
  • OpenSearchQueryAssistToolset (57-78)
  • _invoke (32-50)
  • get_parameterized_one_liner (52-54)
  • get_example_config (71-72)
  • _reload_instructions (74-78)
🔇 Additional comments (4)
tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (3)

1-15: LGTM! Imports are correctly placed and organized.

All imports are at the top of the file as required by the coding guidelines, and they're well-organized with standard library imports followed by project imports.


214-330: LGTM! Comprehensive toolset testing.

The test coverage is thorough:

  • Initialization and configuration validation
  • Template path construction with proper mocking of filesystem operations
  • Inheritance verification
  • Tool configuration consistency checks

The use of mocking for path operations (lines 240-273, 275-302) properly isolates the tests from filesystem dependencies.


398-399: LGTM! Standard pytest entry point.

The main block allows direct execution of this test file, which is a helpful convenience pattern.

experimental/ag-ui/front-end/src/components/ChatAssistant.tsx (1)

1-34: LGTM!

Import structure and type definitions are well-organized and appropriate for the component's functionality.

Comment thread experimental/ag-ui/front-end/src/components/ChatAssistant.tsx
Comment thread experimental/ag-ui/front-end/src/components/ChatAssistant.tsx
Comment thread experimental/ag-ui/front-end/src/components/ChatAssistant.tsx
Comment thread experimental/ag-ui/front-end/src/components/ChatAssistant.tsx Outdated
Comment thread experimental/ag-ui/front-end/src/components/ChatAssistant.tsx
Comment thread tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c636726 and db2ff1a.

📒 Files selected for processing (1)
  • Dockerfile (1 hunks)

Comment thread Dockerfile Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
experimental/ag-ui/server.py (3)

368-374: Message mutation still present.

Lines 372-374 assign msg_tmp = msg then mutate msg_tmp.role, which modifies the original message object in input_data.messages. This was noted in a previous review comment but remains unaddressed.

If this is intentional (e.g., the input messages are not reused), no action needed. Otherwise, create a copy before mutation.

Apply this diff to avoid mutating the original:

         elif msg.role == "tool":
-            msg_tmp = msg
-            msg_tmp.role = "assistant"
-            non_system_messages.append(msg_tmp)
+            from copy import copy
+            msg_copy = copy(msg)
+            msg_copy.role = "assistant"
+            non_system_messages.append(msg_copy)

182-193: Add type safety for result data slicing.

Line 188 assumes chunk.data.get('result', {}).get('data', '') returns a string, but it could be a dict, list, or other type. Direct string slicing [0:200] will raise TypeError for non-string types.

Apply this diff to safely handle different data types:

                         if not front_end_tool_invoked:
                             # TODO [FUTURE]: Render "TodoWrite" tool_name results prettier. Use code block for now.
                             #                 Ideally using TOOL_STEP events.
                             if tool_name == "TodoWrite":
                                 tool_message = _format_todo_write(data=chunk.data)
                             else:
-                                tool_message = f"🔧 {tool_name} result:\n{chunk.data.get('result', {}).get('data', '')[0:200]}..."
+                                result_obj = chunk.data.get('result', {})
+                                data_field = result_obj.get('data', '')
+                                if not isinstance(data_field, str):
+                                    try:
+                                        data_field = json.dumps(data_field)
+                                    except Exception:
+                                        data_field = str(data_field)
+                                snippet = data_field[:200]
+                                tool_message = f"🔧 {tool_name} result:\n{snippet}..."

283-304: Add type check before JSON parsing.

Line 287 calls json.loads(result_data["data"]) without verifying whether the data is a string. If it's already a dict (which is common in tool responses), this will raise TypeError.

Apply this diff to safely handle both string and dict types:

         # Handle different Prometheus response formats
         prometheus_data = result_data
         result_type = "unknown"
         if "data" in result_data:
-            prometheus_data = json.loads(result_data["data"]).get("data")
+            raw_data = result_data["data"]
+            try:
+                parsed = json.loads(raw_data) if isinstance(raw_data, str) else raw_data
+            except (json.JSONDecodeError, TypeError):
+                parsed = {}
+            prometheus_data = parsed.get("data", parsed)
             result_type = prometheus_data.get("resultType", "unknown")
🧹 Nitpick comments (4)
experimental/ag-ui/server.py (4)

80-86: CORS wildcard acceptable for experimental code.

The allow_origins=["*"] setting is appropriate for this experimental AG-UI demo. For production deployments, restrict to specific origins.


89-91: Consider removing unused parameter and standardizing response format.

The request parameter is unused. For consistency, consider:

  • Removing the parameter.
  • Returning a structured response: {"status": "ok"} instead of plain "ok".

Apply this diff:

 @app.get("/api/agui/chat/health")
-def agui_chat_health(request: Request):
-    return JSONResponse(content="ok")
+def agui_chat_health():
+    return JSONResponse(content={"status": "ok"})

200-207: Optional: Use f-string conversion flag for cleaner code.

The static analysis hint suggests using the !s conversion flag instead of explicit str() call.

Apply this diff:

             yield encoder.encode(
                 RunErrorEvent(
                     type=EventType.RUN_ERROR,
-                    message=f"Agent encountered an error: {str(e)}"
+                    message=f"Agent encountered an error: {e!s}"
                 )
             )

299-320: Optional: Move return statement to else block.

Static analysis suggests restructuring the try/except for better clarity by moving the successful return into an else block.

Apply this diff:

     except Exception as e:
         logging.error(f"Error parsing timeseries data: {e}", exc_info=True)
-        # Return a fallback structure
-        return {
+    else:
+        return {
             "title": description,
             "query": query,
             "data": prometheus_data,
             "metadata": metadata
         }
-
-    except Exception as e:
-        logging.error(f"Error parsing timeseries data: {e}", exc_info=True)
-        # Return a fallback structure
-        return {
+    
+    # Fallback for exception case
+    return {
             "title": "Prometheus Query Results (Parse Error)",
             "query": data.get("query", ""),
             "data": {
                 "result": []
             },
             "metadata": {
                 "timestamp": int(time.time()),
                 "source": "Prometheus",
                 "error": str(e)
             }
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db2ff1a and 5e2d87e.

📒 Files selected for processing (1)
  • experimental/ag-ui/server.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server.py
🧬 Code graph analysis (1)
experimental/ag-ui/server.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (20-22)
  • StreamEvents (11-17)
holmes/config.py (5)
  • Config (46-506)
  • load_from_env (169-202)
  • dal (118-121)
  • create_agui_toolcalling_llm (294-305)
  • get_models_list (502-506)
holmes/core/conversations.py (1)
  • build_chat_messages (306-398)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (867-1109)
🪛 Ruff (0.13.3)
experimental/ag-ui/server.py

90-90: Unused function argument: request

(ARG001)


205-205: Use explicit conversion flag

Replace with conversion flag

(RUF010)


299-304: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (5)
experimental/ag-ui/server.py (5)

215-232: LGTM! Clean TODO formatting implementation.

The status icon mapping and formatting logic is well-structured and readable.


235-242: LGTM! Frontend tool matching logic is correct.

The substring checks for "execute_prom" and "execute_ppl" correctly match the frontend tool naming patterns.


323-355: LGTM! Clean event streaming helper implementations.

The _invoke_front_end_tool and _stream_agui_text_message_event async generators correctly yield AG-UI protocol events.


406-408: LGTM! Model endpoint correctly returns list.

The double JSON encoding issue was fixed; the endpoint now returns the model list directly for FastAPI to serialize.


411-419: LGTM! Uvicorn configuration is appropriate.

The logging configuration and server setup are correct for this experimental server.

arikalon1
arikalon1 previously approved these changes Oct 9, 2025
@arikalon1

Copy link
Copy Markdown
Collaborator

@kylehounslow can you please fix the poetry lock issue, so we can merge it?

@kylehounslow

Copy link
Copy Markdown
Contributor Author

@kylehounslow can you please fix the poetry lock issue, so we can merge it?

done!

@kylehounslow

Copy link
Copy Markdown
Contributor Author

Nice work!

I would add to the docs a few examples of how you use it with Prometheus/Opensearch

Thanks for the contribution @kylehounslow !

Thanks for the review! AG-UI server README covers Prometheus setup here and I've added a link to configuring HolmesGPT on OpenSearch Dashboards here

arikalon1
arikalon1 previously approved these changes Oct 9, 2025
@arikalon1

Copy link
Copy Markdown
Collaborator

Nice work!
I would add to the docs a few examples of how you use it with Prometheus/Opensearch
Thanks for the contribution @kylehounslow !

Thanks for the review! AG-UI server README covers Prometheus setup here and I've added a link to configuring HolmesGPT on OpenSearch Dashboards here

Yeah, I saw that
I meant a few questions the user can put in the prompt, that will trigger a Prometheus graph rendering in the ag-ui
Same for Opensearch

Not a must obviously - just will help users see the value faster

@arikalon1

Copy link
Copy Markdown
Collaborator

@kylehounslow can you please check the pre-commit failures?

@kylehounslow

Copy link
Copy Markdown
Contributor Author

@kylehounslow can you please check the pre-commit failures?

On it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (12)
holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 (3)

75-82: Separate JSON body onto new line.

The JSON request body is still concatenated on the same line as the HTTP request. This issue was flagged in a previous review but remains unresolved.

Apply this diff:

-PUT _plugins/_query/settings{"transient":{"plugins":{"ppl":{"enabled":"false"}}}}
+PUT _plugins/_query/settings
+{"transient":{"plugins":{"ppl":{"enabled":"false"}}}}

253-262: Separate JSON body onto new line.

The JSON request body is concatenated on the same line as the endpoint. This issue was flagged in a previous review but remains unresolved.

Apply this diff:

-POST _plugins/_ppl/_explain?format=simple{"query":"source=state_country | where country = 'USA' OR country = 'England' | stats count() by country"}
+POST _plugins/_ppl/_explain?format=simple
+{"query":"source=state_country | where country = 'USA' OR country = 'England' | stats count() by country"}

397-413: Separate JSON body onto new line.

The JSON request body is concatenated on the same line as the endpoint. This issue was flagged in a previous review but remains unresolved.

Apply this diff:

-POST _plugins/_ppl/_explain{"query":"source=state_country | where country = 'USA' OR country = 'England' | stats count() by country"}
+POST _plugins/_ppl/_explain
+{"query":"source=state_country | where country = 'USA' OR country = 'England' | stats count() by country"}
experimental/ag-ui/server-agui.py (4)

90-91: Drop unused request parameter to satisfy Ruff.

agui_chat_health no longer uses the request object, so Ruff (ARG001) still fails. Remove the unused argument so the health check builds cleanly.

-@app.get("/api/agui/chat/health")
-def agui_chat_health(request: Request):
-    return JSONResponse(content="ok")
+@app.get("/api/agui/chat/health")
+def agui_chat_health() -> JSONResponse:
+    return JSONResponse(content="ok")

213-220: Guard tool result formatting against non-string payloads.

Slicing chunk.data.get('result', {}).get('data', '') assumes a string; Prometheus/other tools often return dicts, so this raises TypeError and aborts the stream. Coerce to a string safely before slicing.

-                            if tool_name == "TodoWrite":
-                                tool_message = _format_todo_write(data=chunk.data)
-                            else:
-                                tool_message = f"🔧 {tool_name} result:\n{chunk.data.get('result', {}).get('data', '')[0:200]}..."
+                            if tool_name == "TodoWrite":
+                                tool_message = _format_todo_write(data=chunk.data)
+                            else:
+                                result_payload = chunk.data.get("result", {}) if hasattr(chunk, "data") else {}
+                                data_field = result_payload.get("data", "")
+                                if isinstance(data_field, (dict, list)):
+                                    data_snippet = json.dumps(data_field)
+                                else:
+                                    data_snippet = str(data_field)
+                                tool_message = f"🔧 {tool_name} result:\n{data_snippet[:200]}..."

321-339: Handle dict payloads when parsing Prometheus timeseries.

json.loads(result_data["data"]) blows up when "data" is already a dict (the typical case). That throws, triggers the fallback, and the UI loses charts. Parse conditionally and only call .get on dicts.

-        if "data" in result_data:
-            prometheus_data = json.loads(result_data["data"]).get("data")
-            result_type = prometheus_data.get("resultType", "unknown")
+        if "data" in result_data:
+            raw_payload = result_data["data"]
+            try:
+                parsed_payload = json.loads(raw_payload) if isinstance(raw_payload, str) else raw_payload
+            except (TypeError, json.JSONDecodeError):
+                logging.warning("Failed to parse Prometheus data payload", exc_info=True)
+                parsed_payload = {}
+            prometheus_data = parsed_payload.get("data", parsed_payload)
+            if isinstance(prometheus_data, dict):
+                result_type = prometheus_data.get("resultType", "unknown")
+            else:
+                result_type = "unknown"

394-400: Avoid mutating inbound AG‑UI messages.

Reassigning msg_tmp = msg and then changing msg_tmp.role mutates the original entry in input_data.messages, corrupting upstream state. Copy the message before modifying the role.

-        elif msg.role == "tool":
-            msg_tmp = msg
-            msg_tmp.role = "assistant"
-            non_system_messages.append(msg_tmp)
+        elif msg.role == "tool":
+            msg_copy = deepcopy(msg)
+            msg_copy.role = "assistant"
+            non_system_messages.append(msg_copy)

(remember to add from copy import deepcopy with the other imports below the certificate guard)

experimental/ag-ui/front-end/src/App.css (1)

496-503: Scope the spinner variants so they stop clobbering each other

All three .loading-spinner blocks share the same selector, so the final (14 px white) definition overrides the earlier grey 20 px version that the results placeholder depends on. On a white card the spinner now becomes almost invisible. Please consolidate into a single base rule plus scoped modifiers (e.g., .results-section .loading-spinner, .show-indices-btn .loading-spinner) or rename the variants so each context keeps its intended size/contrast.

Example fix:

-.loading-spinner {
-  width: 20px;
-  height: 20px;
-  border: 2px solid #e9ecef;
-  border-top: 2px solid #6B7280;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
-
-.loading-spinner {
-  width: 16px;
-  height: 16px;
-  border: 2px solid rgba(255, 255, 255, 0.3);
-  border-top: 2px solid white;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
-
-.loading-spinner {
-  width: 14px;
-  height: 14px;
-  border: 2px solid rgba(255, 255, 255, 0.3);
-  border-top: 2px solid white;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
+.loading-spinner {
+  width: 20px;
+  height: 20px;
+  border: 2px solid #e9ecef;
+  border-top: 2px solid #6B7280;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+.show-indices-btn .loading-spinner {
+  width: 16px;
+  height: 16px;
+  border-color: rgba(255, 255, 255, 0.3);
+  border-top-color: #fff;
+}
+
+.indices-discovery-section .loading-spinner {
+  width: 14px;
+  height: 14px;
+  border-color: rgba(255, 255, 255, 0.3);
+  border-top-color: #fff;
+}

Also applies to: 984-991, 1162-1169

experimental/ag-ui/front-end/src/components/MainContent.tsx (4)

1-5: Reuse the shared ObservabilityPage type instead of re-declaring it

ObservabilityPage already lives in App.tsx. Duplicating the union here risks the two definitions drifting apart. Please import the existing type instead of redefining it.

-import React, { useState } from 'react';
-import GraphVisualization from './GraphVisualization';
-import LogsVisualization from './LogsVisualization';
-type ObservabilityPage = 'metrics' | 'logs' | 'traces';
+import React, { useState } from 'react';
+import type { ObservabilityPage } from '../App';
+import GraphVisualization from './GraphVisualization';
+import LogsVisualization from './LogsVisualization';

40-43: Drop the unused abortControllerRef (it fails noUnusedLocals)

abortControllerRef is declared but never read. With the default TypeScript settings (noUnusedLocals: true) this is a build error. Please remove the declaration (and any related code if you bring it back later).

-  const currentQueryRef = React.useRef<string>('');
-  const abortControllerRef = React.useRef<AbortController | null>(null);
+  const currentQueryRef = React.useRef<string>('');

370-449: Include prometheusStatus in the checkPrometheusConnection closure

Because prometheusStatus isn’t in the useCallback deps, the closure freezes it at 'checking'. The guard at Line 377 then short-circuits future checks, so periodic health probes never run unless you force them. Add prometheusStatus to the dependency list (and adjust callers as needed) so the logic sees the latest status.

-  const checkPrometheusConnection = React.useCallback(async (isRetry = false, force = false) => {
+  const checkPrometheusConnection = React.useCallback(async (isRetry = false, force = false) => {
     …
-  }, [prometheusUrl, selectedPage]);
+  }, [prometheusUrl, selectedPage, prometheusStatus]);

520-539: Update the interval effect dependencies

This effect schedules checkPrometheusConnection / checkOpensearchConnection, but the dependency array ignores both callbacks. When the callbacks change (e.g., after adding prometheusStatus to the dependency list) the interval keeps calling the stale version. Add both callbacks to the dependency array so the interval is torn down and rebuilt with the current logic.

-  }, [selectedPage]); // Only depend on selectedPage
+  }, [selectedPage, checkPrometheusConnection, checkOpensearchConnection]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c262635 and 99eb744.

📒 Files selected for processing (24)
  • Dockerfile (1 hunks)
  • experimental/ag-ui/README.md (1 hunks)
  • experimental/ag-ui/front-end/.gitignore (1 hunks)
  • experimental/ag-ui/front-end/README.md (1 hunks)
  • experimental/ag-ui/front-end/public/index.html (1 hunks)
  • experimental/ag-ui/front-end/src/App.css (1 hunks)
  • experimental/ag-ui/front-end/src/App.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/ChatAssistant.css (1 hunks)
  • experimental/ag-ui/front-end/src/components/ChatAssistant.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/ErrorBoundary.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/GraphVisualization.css (1 hunks)
  • experimental/ag-ui/front-end/src/components/GraphVisualization.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/LogsVisualization.css (1 hunks)
  • experimental/ag-ui/front-end/src/components/LogsVisualization.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/components/MainContent.tsx (1 hunks)
  • experimental/ag-ui/front-end/src/index.css (1 hunks)
  • experimental/ag-ui/front-end/src/index.tsx (1 hunks)
  • experimental/ag-ui/front-end/tsconfig.json (1 hunks)
  • experimental/ag-ui/server-agui.py (1 hunks)
  • holmes/config.py (3 hunks)
  • holmes/plugins/toolsets/__init__.py (2 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (1 hunks)
  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (14)
  • Dockerfile
  • experimental/ag-ui/front-end/src/index.css
  • experimental/ag-ui/front-end/public/index.html
  • holmes/plugins/toolsets/init.py
  • experimental/ag-ui/front-end/README.md
  • experimental/ag-ui/front-end/src/components/GraphVisualization.css
  • experimental/ag-ui/front-end/.gitignore
  • experimental/ag-ui/front-end/src/index.tsx
  • experimental/ag-ui/README.md
  • experimental/ag-ui/front-end/tsconfig.json
  • experimental/ag-ui/front-end/src/App.tsx
  • experimental/ag-ui/front-end/src/components/LogsVisualization.css
  • experimental/ag-ui/front-end/src/components/ChatAssistant.css
  • holmes/config.py
🧰 Additional context used
📓 Path-based instructions (4)
holmes/plugins/toolsets/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Toolsets must be located as holmes/plugins/toolsets/{name}.yaml or holmes/plugins/toolsets/{name}/

Files:

  • holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2
  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • holmes/plugins/toolsets/opensearch/opensearch_query_assist.py
  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
  • experimental/ag-ui/server-agui.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Only use pytest markers that are defined in pyproject.toml; never introduce undefined markers/tags

Files:

  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Test files should mirror the source structure under tests/

Files:

  • tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py
🧠 Learnings (1)
📚 Learning: 2025-10-08T00:03:11.993Z
Learnt from: kylehounslow
PR: robusta-dev/holmesgpt#1035
File: experimental/ag-ui/front-end/src/components/ChatAssistant.tsx:118-482
Timestamp: 2025-10-08T00:03:11.993Z
Learning: The directory `experimental/ag-ui/front-end/` contains demo/example front-end code for AG-UI demonstration purposes and does not require thorough code review.

Applied to files:

  • experimental/ag-ui/front-end/src/components/ChatAssistant.tsx
🧬 Code graph analysis (4)
experimental/ag-ui/front-end/src/components/MainContent.tsx (1)
experimental/ag-ui/front-end/src/App.tsx (1)
  • ObservabilityPage (7-7)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (1)
holmes/core/tools.py (8)
  • StructuredToolResult (79-103)
  • StructuredToolResultStatus (52-76)
  • Tool (173-365)
  • ToolParameter (155-161)
  • Toolset (534-768)
  • ToolsetTag (143-146)
  • ToolInvokeContext (164-170)
  • _load_llm_instructions (763-768)
tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (2)
holmes/core/tools.py (6)
  • StructuredToolResult (79-103)
  • StructuredToolResultStatus (52-76)
  • ToolParameter (155-161)
  • ToolsetTag (143-146)
  • ToolInvokeContext (164-170)
  • Toolset (534-768)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (6)
  • PplQueryAssistTool (16-51)
  • OpenSearchQueryAssistToolset (54-77)
  • _invoke (31-47)
  • get_parameterized_one_liner (49-51)
  • get_example_config (68-69)
  • _reload_instructions (71-77)
experimental/ag-ui/server-agui.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (25-27)
  • StreamEvents (15-22)
holmes/config.py (5)
  • Config (46-522)
  • load_from_env (172-205)
  • dal (121-124)
  • create_agui_toolcalling_llm (297-308)
  • get_models_list (518-522)
holmes/core/conversations.py (1)
  • build_chat_messages (307-399)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (888-1128)
🪛 Ruff (0.13.3)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py

31-31: Unused method argument: context

(ARG002)


45-45: Use explicit conversion flag

Replace with conversion flag

(RUF010)

experimental/ag-ui/server-agui.py

90-90: Unused function argument: request

(ARG001)


233-233: Use explicit conversion flag

Replace with conversion flag

(RUF010)


334-339: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (5)
holmes/plugins/toolsets/opensearch/opensearch_query_assist.py (2)

31-47: LGTM—unused context parameter is required by API contract.

The static analysis warning about the unused context parameter (ARG002) is a false positive. The _invoke signature must match the abstract method in the Tool base class (holmes/core/tools.py:231), which requires context: ToolInvokeContext even if not used by this particular implementation.


54-77: LGTM—toolset follows standard pattern.

The toolset initialization and template loading implementation correctly follow the established pattern. The _reload_instructions method properly constructs an absolute file path and formats it as a file:// URI for the template loader.

tests/plugins/toolsets/opensearch/test_opensearch_query_assist.py (3)

17-211: Excellent test coverage for PplQueryAssistTool.

The test suite comprehensively covers:

  • Tool initialization and parameter configuration
  • Various invoke scenarios (valid, empty, missing, complex, list, None queries)
  • One-liner generation for different input types
  • Clear AAA (Arrange-Act-Assert) structure

213-329: Well-structured toolset tests with appropriate mocking.

The test suite properly validates:

  • Toolset initialization properties and configuration
  • Template path construction with detailed mocking
  • Inheritance and interface conformance
  • Proper isolation with targeted mocks for filesystem operations

331-395: Strong integration test coverage.

The integration tests effectively validate:

  • Tool-toolset interaction and data flow
  • Configuration consistency across components
  • Template loading behavior with appropriate mocking
  • End-to-end functionality

Comment thread experimental/ag-ui/front-end/src/components/LogsVisualization.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
experimental/ag-ui/server-agui.py (1)

318-323: Robustly parse timeseries data that may already be a dict.

Line 322 calls json.loads(result_data["data"]) assuming it's a JSON string, but the data field may already be a parsed dict or list. This will raise TypeError if it's not a string.

Apply this diff to handle both cases:

         # Handle different Prometheus response formats
         prometheus_data = result_data
         result_type = "unknown"
         if "data" in result_data:
-            prometheus_data = json.loads(result_data["data"]).get("data")
+            raw_data = result_data["data"]
+            # Parse if string, otherwise use as-is
+            if isinstance(raw_data, str):
+                try:
+                    parsed = json.loads(raw_data)
+                except json.JSONDecodeError:
+                    logging.warning(f"Failed to parse data field as JSON: {raw_data}")
+                    parsed = {}
+            else:
+                parsed = raw_data
+            prometheus_data = parsed.get("data", parsed) if isinstance(parsed, dict) else parsed
             result_type = prometheus_data.get("resultType", "unknown")
🧹 Nitpick comments (3)
experimental/ag-ui/server-agui.py (3)

89-91: Remove unused request parameter.

The request parameter is not used in this health check endpoint.

Apply this diff:

-def agui_chat_health(request: Request):
+def agui_chat_health():
     return JSONResponse(content={"status": "ok"})

55-71: Consider adding type hints to helper functions.

As per coding guidelines, type hints are required. Several helper functions lack return type annotations:

  • init_logging() (line 55)
  • _format_todo_write(data) (line 242)
  • _parse_query(data) (line 275)
  • _should_graph_timeseries_data(tool_name) (line 282)
  • _is_tool_result_message(input_data) (line 383)

Example for init_logging:

def init_logging() -> None:
    ...

Example for _is_tool_result_message:

def _is_tool_result_message(input_data: RunAgentInput) -> bool:
    ...

Also applies to: 242-255, 275-279, 282-287, 383-384


233-233: Optional: Address static analysis style suggestions.

Two minor style improvements suggested by static analysis:

  1. Line 233: Use explicit conversion flag instead of str(e):
message=f"Agent encountered an error: {e!s}",
  1. Lines 334-339: Move the return statement to an else block after the except for clearer structure (TRY300).

Also applies to: 334-339

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99eb744 and 4905aa9.

📒 Files selected for processing (1)
  • experimental/ag-ui/server-agui.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server-agui.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: kylehounslow
PR: robusta-dev/holmesgpt#1035
File: experimental/ag-ui/front-end/src/components/ChatAssistant.tsx:118-482
Timestamp: 2025-10-08T00:03:11.993Z
Learning: The directory `experimental/ag-ui/front-end/` contains demo/example front-end code for AG-UI demonstration purposes and does not require thorough code review.
🧬 Code graph analysis (1)
experimental/ag-ui/server-agui.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (25-27)
  • StreamEvents (15-22)
holmes/config.py (5)
  • Config (46-522)
  • load_from_env (172-205)
  • dal (121-124)
  • create_agui_toolcalling_llm (297-308)
  • get_models_list (518-522)
holmes/core/conversations.py (1)
  • build_chat_messages (307-399)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (888-1128)
🪛 Ruff (0.13.3)
experimental/ag-ui/server-agui.py

90-90: Unused function argument: request

(ARG001)


233-233: Use explicit conversion flag

Replace with conversion flag

(RUF010)


334-339: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (1)
experimental/ag-ui/server-agui.py (1)

94-239: Well-structured AG-UI integration.

The main endpoint successfully translates HolmesGPT outputs into AG-UI events with clear separation:

  • Input conversion (lines 105-120)
  • Event streaming with proper async generators
  • Conditional front-end tool invocation based on tool types
  • Comprehensive error handling

The integration with config.create_agui_toolcalling_llm and build_chat_messages leverages existing Holmes infrastructure effectively.

Comment thread experimental/ag-ui/server-agui.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
experimental/ag-ui/server-agui.py (1)

210-221: Fix unsafe data slicing to prevent TypeError.

Line 216 slices chunk.data.get('result', {}).get('data', '') directly, which will raise TypeError if data is a dict, list (non-string sequence), or other non-string object. Previous reviews flagged this issue; it remains unaddressed.

Apply this diff to safely extract and format the data:

                         if not front_end_tool_invoked:
                             # TODO [FUTURE]: Render "TodoWrite" tool_name results prettier. Use code block for now.
                             #                 Ideally using TOOL_STEP events.
                             if tool_name == "TodoWrite":
                                 tool_message = _format_todo_write(data=chunk.data)
                             else:
-                                tool_message = f"🔧 {tool_name} result:\n{chunk.data.get('result', {}).get('data', '')[0:200]}..."
+                                # Safely extract and format result data
+                                result_obj = chunk.data.get('result', {})
+                                data_field = result_obj.get('data', '')
+                                if isinstance(data_field, (dict, list)):
+                                    try:
+                                        data_str = json.dumps(data_field)
+                                    except Exception:
+                                        data_str = str(data_field)
+                                else:
+                                    data_str = str(data_field) if data_field is not None else ''
+                                snippet = data_str[:200]
+                                tool_message = f"🔧 {tool_name} result:\n{snippet}..."
 
                             async for event in _stream_agui_text_message_event(
                                 message=tool_message
🧹 Nitpick comments (5)
experimental/ag-ui/server-agui.py (5)

74-87: Restrict CORS origins before production deployment.

The current CORS configuration allows all origins (allow_origins=["*"]), which is acceptable for an experimental/demo server but poses security risks in production. Before deploying to production, restrict allow_origins to specific trusted domains.


90-92: Remove unused request parameter.

The request parameter is not used in the health check endpoint. Remove it to clean up the function signature.

Apply this diff:

 @app.get("/api/agui/chat/health")
-def agui_chat_health(request: Request):
+def agui_chat_health():
     return JSONResponse(content="ok")

259-273: Consider more robust tool matching for production.

The current string-based matching ("execute_prom" in fe_tool.name) works for the experimental/demo context but may be fragile if tool names change. For production deployment, consider using explicit tool identifiers or enums.


445-447: Consider renaming response key for clarity.

The response key "model_name" is singular but contains a list of models. Consider renaming it to "model_names" or "models" for better clarity.

Apply this diff:

 @app.get("/api/model")
 def get_model():
-    return {"model_name": config.get_models_list()}
+    return {"models": config.get_models_list()}

450-460: Consider multiprocess deployment for production scaling.

The current setup runs a single uvicorn process, which is appropriate for experimental/demo purposes. For production deployment with higher load, consider using uvicorn's built-in multiprocess manager (--workers flag) or deploying behind a reverse proxy with multiple workers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4905aa9 and 6ec74ba.

📒 Files selected for processing (1)
  • experimental/ag-ui/server-agui.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server-agui.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: kylehounslow
PR: robusta-dev/holmesgpt#1035
File: experimental/ag-ui/front-end/src/components/ChatAssistant.tsx:118-482
Timestamp: 2025-10-08T00:03:11.993Z
Learning: The directory `experimental/ag-ui/front-end/` contains demo/example front-end code for AG-UI demonstration purposes and does not require thorough code review.
🧬 Code graph analysis (1)
experimental/ag-ui/server-agui.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (25-27)
  • StreamEvents (15-22)
holmes/config.py (5)
  • Config (46-522)
  • load_from_env (172-205)
  • dal (121-124)
  • create_agui_toolcalling_llm (297-308)
  • get_models_list (518-522)
holmes/core/conversations.py (1)
  • build_chat_messages (307-399)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (888-1128)
🪛 Ruff (0.13.3)
experimental/ag-ui/server-agui.py

91-91: Unused function argument: request

(ARG001)


234-234: Use explicit conversion flag

Replace with conversion flag

(RUF010)


335-340: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (15)
experimental/ag-ui/server-agui.py (15)

1-11: LGTM! Certificate setup pattern is correct.

The certificate setup before network imports is correct, and the environment variable name CERTIFICATE matches the official HolmesGPT documentation as confirmed in previous review discussions.


12-53: LGTM! Import organization follows best practices.

All imports are properly placed after the certificate setup block, and the necessary dependencies for the AG-UI server are correctly imported.


56-72: LGTM! Logging configuration is well-structured.

The colored logging setup with environment-driven log levels and httpx noise reduction is appropriate for a development/debugging server.


95-104: LGTM! Endpoint setup and early return logic is correct.

The accept header parsing and early return for unsupported tool result messages is appropriate. The bounds check in _is_tool_result_message was correctly added in a previous iteration.


106-121: LGTM! Chat request validation and message building is correct.

The conversion from AG-UI input to Holmes chat request, validation of the ask field, and message building using the existing build_chat_messages function is properly implemented.


138-161: LGTM! AI message processing is correctly implemented.

The event type extraction with safety checks and content streaming via _stream_agui_text_message_event is properly implemented.


162-166: LGTM! Start tool event processing is correct.

The tool name extraction and message formatting for tool start events is properly implemented.


229-236: LGTM! Error handling is appropriately implemented.

The try-except block properly catches exceptions during streaming, logs with traceback, and emits a RUN_ERROR event to inform the client. This is the correct pattern for handling errors in async generators.


238-240: LGTM! StreamingResponse setup is correct.

The StreamingResponse with the event generator and encoder-determined content type is properly configured for streaming AG-UI events.


243-256: LGTM! TodoWrite formatting is well-implemented.

The function safely extracts todo items and formats them with status icons for clear presentation to users.


276-280: LGTM! Query parsing is correctly implemented.

The function safely extracts the query field using nested .get() calls with appropriate defaults.


283-288: LGTM! Timeseries data check is correctly implemented.

The function clearly checks for Prometheus tools and includes a comment explaining the current limitation. This is appropriate for the experimental scope.


357-370: LGTM! Front-end tool invocation is correctly implemented.

The function properly yields the sequence of AG-UI tool call events (START, ARGS, END) with correct serialization of arguments.


373-381: LGTM! Text message streaming is correctly implemented.

The function generates a unique message ID and properly yields the sequence of AG-UI text message events with correct structure.


384-385: LGTM! Tool result message check is correctly implemented.

The function properly checks the list length before accessing the last element, preventing IndexError as addressed in previous reviews.

Comment thread experimental/ag-ui/server-agui.py
Comment thread experimental/ag-ui/server-agui.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
experimental/ag-ui/server-agui.py (2)

210-217: Tool result slicing will crash on non-string payloads.

chunk.data["result"]["data"] is often a dict/list; slicing it raises TypeError, so TOOL_RESULT streaming breaks. Convert to a string safely before truncating.

-            else:
-                tool_message = f"🔧 {tool_name} result:\n{chunk.data.get('result', {}).get('data', '')[0:200]}..."
+            else:
+                raw_data = chunk.data.get("result", {}).get("data", "")
+                if not isinstance(raw_data, str):
+                    try:
+                        raw_data = json.dumps(raw_data)
+                    except Exception:  # pragma: no cover
+                        raw_data = str(raw_data)
+                tool_message = (
+                    f"🔧 {tool_name} result:\n{raw_data[:200]}..."
+                    if raw_data
+                    else f"🔧 {tool_name} result:"
+                )

320-340: Handle non-string Prometheus payloads before json.loads.

result_data["data"] is already a dict for many Prometheus responses; calling json.loads on it throws TypeError, so time-series parsing breaks. Please normalize the payload first and keep a safe fallback:

-        prometheus_data = result_data
-        result_type = "unknown"
-        if "data" in result_data:
-            prometheus_data = json.loads(result_data["data"]).get("data")
-            result_type = prometheus_data.get("resultType", "unknown")
+        prometheus_data = result_data
+        result_type = "unknown"
+        if "data" in result_data:
+            raw = result_data["data"]
+            try:
+                parsed = json.loads(raw) if isinstance(raw, str) else raw
+            except Exception:
+                logging.warning("Failed to parse Prometheus data", exc_info=True)
+                parsed = {}
+            prometheus_data = parsed.get("data", parsed)
+            if isinstance(prometheus_data, dict):
+                result_type = prometheus_data.get("resultType", "unknown")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4905aa9 and 87ae8e5.

📒 Files selected for processing (1)
  • experimental/ag-ui/server-agui.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for formatting and linting (configured in pyproject.toml) for all Python code
Type hints are required; code should pass mypy (configured in pyproject.toml)
ALWAYS place Python imports at the top of the file, not inside functions or methods

Files:

  • experimental/ag-ui/server-agui.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: kylehounslow
PR: robusta-dev/holmesgpt#1035
File: experimental/ag-ui/front-end/src/components/ChatAssistant.tsx:118-482
Timestamp: 2025-10-08T00:03:11.993Z
Learning: The directory `experimental/ag-ui/front-end/` contains demo/example front-end code for AG-UI demonstration purposes and does not require thorough code review.
🧬 Code graph analysis (1)
experimental/ag-ui/server-agui.py (6)
holmes/utils/cert_utils.py (1)
  • add_custom_certificate (29-40)
holmes/utils/stream.py (2)
  • StreamMessage (25-27)
  • StreamEvents (15-22)
holmes/config.py (5)
  • Config (46-522)
  • load_from_env (172-205)
  • dal (121-124)
  • create_agui_toolcalling_llm (297-308)
  • get_models_list (518-522)
holmes/core/conversations.py (1)
  • build_chat_messages (307-399)
holmes/core/models.py (1)
  • ChatRequest (232-233)
holmes/core/tool_calling_llm.py (1)
  • call_stream (888-1128)
🪛 Ruff (0.13.3)
experimental/ag-ui/server-agui.py

1-1: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


91-91: Unused function argument: request

(ARG001)


234-234: Use explicit conversion flag

Replace with conversion flag

(RUF010)


335-340: Consider moving this statement to an else block

(TRY300)

Comment thread experimental/ag-ui/server-agui.py
Comment thread experimental/ag-ui/server-agui.py
Comment thread experimental/ag-ui/server-agui.py
@kylehounslow
kylehounslow requested a review from arikalon1 October 9, 2025 22:21
@kylehounslow

kylehounslow commented Oct 9, 2025 •

Copy link
Copy Markdown
Contributor Author

mypy and ruff fixes have been applied. Looks like things are passing on my forked repo workflows: https://github.com/kylehounslow/holmesgpt/actions/runs/18390467516

@arikalon1
arikalon1 merged commit 6fe8ee3 into HolmesGPT:master Oct 10, 2025
7 checks passed
@arikalon1

Copy link
Copy Markdown
Collaborator

Thanks @kylehounslow !
Thanks for brining ag-ui into Holmes - looking forward to the next iterations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add AG-UI Protocol Support

3 participants