Skip to content

show evals summary at end of pytest run - #620

Merged
aantn merged 25 commits into
masterfrom
show-evals-summary
Jul 22, 2025
Merged

aantn merged 25 commits into
masterfrom
show-evals-summary

Conversation

@aantn

@aantn aantn commented Jul 11, 2025

Copy link
Copy Markdown
Collaborator

No description provided.

@aantn
aantn requested a review from Sheeproid July 11, 2025 15:59
@coderabbitai

coderabbitai Bot commented Jul 11, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

This change refactors LLM evaluation test reporting by overhauling the pytest terminal summary logic. It now collects test results from pytest's internal stats, attaches detailed metadata to each test via user properties, generates markdown and console summary reports, and uses GPT-4o via LiteLLM for automated failure analysis. Documentation and CI/CD workflow environment variables are updated accordingly.

Changes

File(s) Change Summary
tests/llm/conftest.py Refactored pytest terminal summary: collects results from pytest stats, adds TestResult dataclass, LLM-based analysis, markdown/Rich reporting, and helper functions.
tests/llm/test_ask_holmes.py
tests/llm/test_investigate.py
tests/llm/test_workload_health.py
Updated test functions to accept request fixture and append detailed test metadata to request.node.user_properties.
.github/workflows/llm-evaluation.yaml Removed PUSH_EVALS_TO_BRAINTRUST env variable from CI workflow.
CLAUDE.md
docs/development/evals/index.md
Updated documentation: clarified and added environment variables for Braintrust integration and reporting.
docs/development/evals/reporting.md Removed references to PUSH_EVALS_TO_BRAINTRUST from documentation and env variable tables.

Sequence Diagram(s)

sequenceDiagram
    participant Pytest
    participant TestFunction
    participant conftest.py
    participant LiteLLM
    participant Braintrust

    Pytest->>TestFunction: Run test (with request)
    TestFunction->>TestFunction: Append metadata to request.node.user_properties
    Pytest->>conftest.py: Call pytest_terminal_summary
    conftest.py->>conftest.py: Collect test results from terminalreporter.stats
    conftest.py->>conftest.py: Build TestResult objects
    conftest.py->>conftest.py: Generate markdown & Rich reports
    alt If Braintrust API key present
        conftest.py->>Braintrust: Generate links (if enabled)
    end
    alt For failed tests
        conftest.py->>LiteLLM: Request GPT-4o analysis via API
        LiteLLM-->>conftest.py: Return failure analysis
    end
    conftest.py->>Pytest: Print summary table and markdown report
Loading

Estimated code review effort

3 (~45 minutes)

Possibly related PRs

Suggested reviewers

  • moshemorad
  • Sheeproid
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@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

🧹 Nitpick comments (4)
tests/llm/utils/summary_plugin.py (4)

5-5: Remove duplicate import statement.

textwrap is imported twice - on line 5 and again on line 184 inside the method.

Remove the duplicate import on line 184:

     def _print_summary_table(self, test_results: Dict[str, TestResult]):
         """Print formatted summary table"""
-        import textwrap
-
         table_data = []

Also applies to: 184-184


254-260: Make the LLM model configurable.

The model "gpt-4o" is hardcoded. Consider making it configurable via environment variable to match the pattern used in other test files.

         try:
             response = completion(
-                model="gpt-4o",
+                model=os.environ.get("MODEL", "gpt-4o"),
                 messages=[{"role": "user", "content": prompt}],
                 max_tokens=200,
                 temperature=0.1,
             )

Don't forget to import os at the top of the file:

+import os
 from typing import Dict, List, Optional

234-263: Add error handling for LLM configuration issues.

The _get_llm_analysis method might fail if LiteLLM is not properly configured with API keys. Consider adding more specific error handling to provide better debugging information.

The current error handling is basic. Consider catching specific exceptions:

         try:
             response = completion(
                 model="gpt-4o",
                 messages=[{"role": "user", "content": prompt}],
                 max_tokens=200,
                 temperature=0.1,
             )
             return response.choices[0].message.content.strip()
+        except ImportError as e:
+            return f"LiteLLM not available: {e}"
+        except AttributeError as e:
+            return f"LLM configuration error: {e}"
         except Exception as e:
-            return f"Analysis failed: {e}"
+            return f"Analysis failed: {type(e).__name__}: {e}"

57-59: Consider making debug output conditional.

The debug print statement will appear for every test, which might clutter the output.

Consider making the debug output conditional based on an environment variable:

             # Debug output
-            print(
-                f"[DEBUG] Plugin captured test {test_id} ({test_name}): expected='{expected[:50]}...', actual='{actual[:50]}...'"
-            )
+            if os.environ.get("HOLMES_DEBUG"):
+                print(
+                    f"[DEBUG] Plugin captured test {test_id} ({test_name}): expected='{expected[:50]}...', actual='{actual[:50]}...'"
+                )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 431cd33 and 0aeca2c.

📒 Files selected for processing (4)
  • tests/llm/conftest.py (1 hunks)
  • tests/llm/test_ask_holmes.py (2 hunks)
  • tests/llm/test_investigate.py (2 hunks)
  • tests/llm/utils/summary_plugin.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
tests/llm/conftest.py (1)
tests/llm/utils/summary_plugin.py (1)
  • pytest_configure (265-269)
tests/llm/test_investigate.py (1)
tests/llm/utils/mock_utils.py (1)
  • InvestigateTestCase (65-69)
tests/llm/test_ask_holmes.py (1)
tests/llm/utils/mock_utils.py (1)
  • AskHolmesTestCase (60-62)
tests/llm/utils/summary_plugin.py (2)
holmes/core/tool_calling_llm.py (1)
  • call (237-368)
tests/llm/conftest.py (1)
  • pytest_configure (14-19)
🪛 GitHub Actions: Evaluate LLM test cases
tests/llm/utils/summary_plugin.py

[error] 3-3: ModuleNotFoundError: No module named 'tabulate'. The test run failed due to missing dependency.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Pre-commit checks
  • GitHub Check: Pre-commit checks
🔇 Additional comments (3)
tests/llm/conftest.py (1)

10-20: LGTM! Clean plugin registration.

The implementation correctly registers the custom summary plugin and reduces logging noise during test runs.

tests/llm/test_investigate.py (1)

95-186: LGTM! Proper integration with summary plugin.

The test correctly stores expected output, actual output, and tools called as user properties for the summary plugin to collect.

tests/llm/test_ask_holmes.py (1)

63-172: LGTM! Consistent implementation with test_investigate.

The test properly stores test metadata as user properties for the summary plugin.

Comment thread tests/llm/utils/summary_plugin.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: 3

♻️ Duplicate comments (1)
tests/llm/utils/summary_plugin.py (1)

3-3: Address the missing dependency issue.

The tabulate dependency issue flagged in the previous review hasn't been resolved yet. This will cause runtime failures when the plugin is used.

🧹 Nitpick comments (4)
tests/llm/utils/summary_plugin.py (4)

176-176: Remove redundant import.

textwrap is already imported at the module level (line 5).

-        import textwrap

246-252: Improve error handling for LLM calls.

The LLM call should handle specific exceptions and provide more informative error messages.

         try:
             response = completion(
                 model="gpt-4o",
                 messages=[{"role": "user", "content": prompt}],
                 max_tokens=200,
                 temperature=0.1,
             )
-            return response.choices[0].message.content.strip()
-        except Exception as e:
-            return f"Analysis failed: {e}"
+            if response.choices and response.choices[0].message.content:
+                return response.choices[0].message.content.strip()
+            else:
+                return "Analysis failed: Empty response from LLM"
+        except ImportError:
+            return "Analysis failed: LiteLLM not available"
+        except Exception as e:
+            return f"Analysis failed: {type(e).__name__}: {str(e)}"

268-268: Add missing newline at end of file.

Python files should end with a newline character for better compatibility.

         config.pluginmanager.unregister(config.summary_plugin)
+

21-91: Consider adding configuration options for the plugin.

The plugin currently captures all "holmes" tests but lacks configuration options for controlling its behavior (e.g., enabling/disabling LLM analysis, controlling output verbosity).

Consider adding configuration options that can be set via pytest command line or configuration files:

  • --holmes-summary to enable/disable the plugin
  • --holmes-analysis to enable/disable LLM analysis
  • --holmes-debug to control debug output
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0aeca2c and b2e7b29.

📒 Files selected for processing (1)
  • tests/llm/utils/summary_plugin.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tests/llm/utils/summary_plugin.py (2)
holmes/core/tool_calling_llm.py (1)
  • call (237-368)
tests/llm/conftest.py (1)
  • pytest_configure (14-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Pre-commit checks
🔇 Additional comments (1)
tests/llm/utils/summary_plugin.py (1)

143-150: Add bounds checking for array access.

The code assumes specific array indices exist without validation, which could cause IndexError.

             # Look for ** EXPECTED ** section
             if "** EXPECTED **" in all_output:
                 lines = all_output.split("\n")
                 for i, line in enumerate(lines):
-                    if "** EXPECTED **" in line and i + 1 < len(lines):
-                        expected_line = lines[i + 1]
-                        if expected_line.startswith("-  "):
-                            expected = expected_line[3:]  # Remove "-  " prefix
-                        break
+                    if "** EXPECTED **" in line and i + 1 < len(lines):
+                        expected_line = lines[i + 1]
+                        if expected_line.startswith("-  ") and len(expected_line) > 3:
+                            expected = expected_line[3:]  # Remove "-  " prefix
+                        break

Likely an incorrect or invalid review comment.

Comment thread tests/llm/utils/summary_plugin.py Outdated
Comment thread tests/llm/utils/summary_plugin.py Outdated
Comment thread tests/llm/utils/summary_plugin.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 (3)
tests/llm/utils/summary_plugin.py (3)

68-70: Remove or make debug output conditional

Debug print statements should not be left in production code as they clutter the test output.

Either remove the debug output:

-            # Debug output
-            print(
-                f"[DEBUG] Plugin captured test {test_id} ({test_name}): expected='{expected[:50]}...', actual='{actual[:50]}...'"
-            )

Or make it conditional based on verbosity:

-            # Debug output
-            print(
-                f"[DEBUG] Plugin captured test {test_id} ({test_name}): expected='{expected[:50]}...', actual='{actual[:50]}...'"
-            )
+            # Debug output (only in verbose mode)
+            if hasattr(item.config.option, 'verbose') and item.config.option.verbose > 1:
+                print(
+                    f"[DEBUG] Plugin captured test {test_id} ({test_name}): expected='{expected[:50]}...', actual='{actual[:50]}...'"
+                )

104-110: Add error handling for test ID extraction

The string parsing logic is fragile and could raise IndexError if test names don't follow the expected format.

 def _extract_test_id(self, test_name: str) -> str:
     """Extract test ID from test name like test_ask_holmes[01_how_many_pods]"""
-    if "[" in test_name and "]" in test_name:
-        test_case = test_name.split("[")[1].split("]")[0]
-        # Extract number from start of test case name
-        return test_case.split("_")[0] if "_" in test_case else test_case
-    return "unknown"
+    try:
+        if "[" in test_name and "]" in test_name:
+            test_case = test_name.split("[")[1].split("]")[0]
+            # Extract number from start of test case name
+            return test_case.split("_")[0] if "_" in test_case else test_case
+    except (IndexError, AttributeError):
+        pass
+    return "unknown"

112-119: Add error handling for test name extraction

Similar fragile string parsing that needs protection against malformed input.

 def _extract_test_name(self, test_name: str) -> str:
     """Extract readable test name"""
-    if "[" in test_name and "]" in test_name:
-        test_case = test_name.split("[")[1].split("]")[0]
-        # Remove number prefix and convert underscores to spaces
-        parts = test_case.split("_")[1:] if "_" in test_case else [test_case]
-        return "_".join(parts)
-    return test_name
+    try:
+        if "[" in test_name and "]" in test_name:
+            test_case = test_name.split("[")[1].split("]")[0]
+            # Remove number prefix and convert underscores to spaces
+            parts = test_case.split("_")[1:] if "_" in test_case else [test_case]
+            return "_".join(parts)
+    except (IndexError, AttributeError):
+        pass
+    return test_name
🧹 Nitpick comments (1)
tests/llm/utils/summary_plugin.py (1)

256-285: Consider making the LLM model configurable

The LLM analysis implementation is excellent with well-structured prompts and proper error handling. However, the model is hardcoded to "gpt-4o".

Consider making the model configurable via environment variable or pytest configuration:

     def _get_llm_analysis(self, result: TestResult) -> str:
         """Get LLM analysis of test failure"""
+        import os
+        model = os.getenv("HOLMES_TEST_ANALYSIS_MODEL", "gpt-4o")
+        
         prompt = textwrap.dedent(f"""\
             Analyze this failed eval for an AIOps agent why it failed.
             TEST: {result.test_name}
             EXPECTED: {result.expected}
             ACTUAL: {result.actual}
             TOOLS CALLED: {', '.join(result.tools_called)}
             ERROR: {result.error_message or 'Test assertion failed'}

             LOGS:
             {result.logs if result.logs else 'No logs available'}

             Please provide a concise analysis (2-3 sentences) and categorize this as one of:
             - Problem with mock data - the test is failing due to incorrect or incomplete mock data, but the agent itself did the correct queries you would expect it to do
             - Setup issue - the test is failing due to an issue with the test setup, such as missing tools or incorrect before_test/after_test configuration
             - Real failure - the test is failing because the agent did not perform as expected, and this is a real issue that needs to be fixed
             """)

         try:
             response = completion(
-                model="gpt-4o",
+                model=model,
                 messages=[{"role": "user", "content": prompt}],
                 max_tokens=200,
                 temperature=0.1,
             )
             return response.choices[0].message.content.strip()
         except Exception as e:
             return f"Analysis failed: {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 102ddfd and b980fad.

📒 Files selected for processing (1)
  • tests/llm/utils/summary_plugin.py (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
tests/llm/utils/summary_plugin.py (1)

Learnt from: Sheeproid
PR: #586
File: tests/llm/fixtures/test_ask_holmes/03_what_is_the_command_to_port_forward/test_case.yaml:4-4
Timestamp: 2025-07-02T10:27:17.231Z
Learning: In LLM-as-judge test cases for HolmesGPT, expected outputs should be descriptive rather than prescriptive when testing for flexible responses like port numbers. Using specific values in expected outputs can cause unnecessary test failures when the AI generates different but equally valid responses.

🧬 Code Graph Analysis (1)
tests/llm/utils/summary_plugin.py (2)
holmes/core/tool_calling_llm.py (1)
  • call (244-384)
tests/llm/conftest.py (1)
  • pytest_configure (14-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: llm_evals
  • GitHub Check: Pre-commit checks
  • GitHub Check: Pre-commit checks
🔇 Additional comments (5)
tests/llm/utils/summary_plugin.py (5)

10-22: Well-structured dataclass design

The TestResult dataclass is properly typed with appropriate optional fields and default values.


24-51: Proper pytest plugin structure with appropriate hooks

The plugin correctly implements pytest hooks for test lifecycle management and log capture.


121-172: Robust test data extraction with multiple fallbacks

The method effectively extracts test data from multiple sources (user properties and captured output) with appropriate fallback mechanisms.


174-255: Excellent summary table implementation with Rich formatting

The summary generation provides a comprehensive view of test results with proper text wrapping and visual indicators. The integration of LLM analysis for failed tests adds significant value.


287-298: Proper pytest plugin registration implementation

The plugin registration and unregistration functions correctly follow pytest conventions.

@aantn
aantn enabled auto-merge (squash) July 21, 2025 13:56

@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

🧹 Nitpick comments (4)
tests/llm/conftest.py (4)

14-16: Add error handling for plugin registration

The current implementation doesn't handle the case where the plugin might already be registered in the plugin manager. Consider checking if it's already registered before attempting to register it again.

-    if not hasattr(config, "summary_plugin"):
-        config.summary_plugin = SummaryPlugin()
-        config.pluginmanager.register(config.summary_plugin, "summary_plugin")
+    if not hasattr(config, "summary_plugin"):
+        config.summary_plugin = SummaryPlugin()
+        if not config.pluginmanager.is_registered(config.summary_plugin):
+            config.pluginmanager.register(config.summary_plugin, "summary_plugin")

22-26: Improve plugin unregistration safety

The unconfigure function should verify the plugin is registered before attempting to unregister it to avoid potential errors.

 def pytest_unconfigure(config):
     """Unregister the plugin"""
     if hasattr(config, "summary_plugin"):
-        config.pluginmanager.unregister(config.summary_plugin)
+        if config.pluginmanager.is_registered(config.summary_plugin):
+            config.pluginmanager.unregister(config.summary_plugin)
+        delattr(config, "summary_plugin")

328-339: Improve error handling in test name extraction

The current implementation uses a bare except clause for specific exceptions. Be more explicit about exception handling and avoid catching all exceptions.

 def _extract_test_name_from_nodeid(nodeid: str) -> str:
     """Extract readable test name from nodeid"""
-    try:
-        if "[" in nodeid and "]" in nodeid:
-            test_case = nodeid.split("[")[1].split("]")[0]
-            # Remove number prefix and convert underscores to spaces
-            parts = test_case.split("_")[1:] if "_" in test_case else [test_case]
-            return "_".join(parts)
-    except (IndexError, AttributeError):
-        pass
-    return nodeid.split("::")[-1] if "::" in nodeid else nodeid
+    if "[" in nodeid and "]" in nodeid:
+        try:
+            test_case = nodeid.split("[")[1].split("]")[0]
+            # Remove number prefix and convert underscores to spaces
+            parts = test_case.split("_")[1:] if "_" in test_case else [test_case]
+            return "_".join(parts)
+        except IndexError:
+            # Fall through to default handling
+            pass
+    return nodeid.split("::")[-1] if "::" in nodeid else nodeid

183-318: Consider refactoring pytest_terminal_summary for better maintainability

The function is quite long and handles multiple responsibilities. Consider breaking it down into smaller, focused functions for better maintainability and testability.

Consider refactoring into separate functions:

  • _collect_test_results(terminalreporter) - Extract test results from stats
  • _calculate_summary_stats(test_results) - Calculate pass/fail/regression counts
  • _generate_markdown_report(test_results, stats) - Generate the markdown content
  • _write_report_files(markdown, regressions) - Handle file writing with error handling

This would make the code more modular, easier to test, and easier to maintain.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7eac5e and 8f1ae91.

📒 Files selected for processing (1)
  • tests/llm/conftest.py (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Pre-commit checks
  • GitHub Check: llm_evals
  • GitHub Check: Pre-commit checks

Comment thread tests/llm/conftest.py
Comment thread tests/llm/conftest.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 (2)
tests/llm/conftest.py (2)

287-292: Add error handling for score conversions.

The float conversions could raise ValueError if the user properties contain invalid data. This matches a previous review comment that should be addressed for robustness.

-            expected_correctness_score = float(
-                user_props.get("expected_correctness_score", 1.0)
-            )
-            actual_correctness_score = float(
-                user_props.get("actual_correctness_score", 0.0)
-            )
+            try:
+                expected_correctness_score = float(
+                    user_props.get("expected_correctness_score", 1.0)
+                )
+                actual_correctness_score = float(
+                    user_props.get("actual_correctness_score", 0.0)
+                )
+            except (ValueError, TypeError):
+                # Skip test if scores are invalid
+                continue

422-428: Add error handling for file operations.

File write operations should include error handling to prevent crashes during report generation, matching a previous review comment.

-        with open("evals_report.txt", "w", encoding="utf-8") as file:
-            file.write(markdown)
-
-        # Write regressions file if needed
-        if total_regressions > 0:
-            with open("regressions.txt", "w", encoding="utf-8") as file:
-                file.write(f"{total_regressions}")
+        try:
+            with open("evals_report.txt", "w", encoding="utf-8") as file:
+                file.write(markdown)
+        except IOError as e:
+            print(f"Failed to write evals report: {e}")
+
+        # Write regressions file if needed
+        if total_regressions > 0:
+            try:
+                with open("regressions.txt", "w", encoding="utf-8") as file:
+                    file.write(f"{total_regressions}")
+            except IOError as e:
+                print(f"Failed to write regressions file: {e}")
🧹 Nitpick comments (1)
tests/llm/conftest.py (1)

521-524: Apply static analysis suggestion for cleaner code.

The ternary operator suggestion from Ruff would make the code more concise.

-        if "PASS" in result.pass_fail:
-            status = "[green]✓[/green]"
-        else:
-            status = "[red]✗[/red]"
+        status = "[green]✓[/green]" if "PASS" in result.pass_fail else "[red]✗[/red]"
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f1ae91 and 325a04e.

📒 Files selected for processing (5)
  • .github/workflows/llm-evaluation.yaml (0 hunks)
  • CLAUDE.md (1 hunks)
  • docs/development/evals/index.md (1 hunks)
  • docs/development/evals/reporting.md (0 hunks)
  • tests/llm/conftest.py (2 hunks)
💤 Files with no reviewable changes (2)
  • .github/workflows/llm-evaluation.yaml
  • docs/development/evals/reporting.md
✅ Files skipped from review due to trivial changes (2)
  • CLAUDE.md
  • docs/development/evals/index.md
🧰 Additional context used
🧠 Learnings (1)
tests/llm/conftest.py (2)

Learnt from: nherment
PR: #610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025-07-08T08:45:41.069Z
Learning: When suggesting improvements to environment variable handling in robusta-dev/holmesgpt, check first if validation logic already exists rather than reimplementing it.

Learnt from: Sheeproid
PR: #586
File: tests/llm/fixtures/test_ask_holmes/03_what_is_the_command_to_port_forward/test_case.yaml:4-4
Timestamp: 2025-07-02T10:27:17.231Z
Learning: In LLM-as-judge test cases for HolmesGPT, expected outputs should be descriptive rather than prescriptive when testing for flexible responses like port numbers. Using specific values in expected outputs can cause unnecessary test failures when the AI generates different but equally valid responses.

🧬 Code Graph Analysis (1)
tests/llm/conftest.py (4)
holmes/core/llm.py (2)
  • completion (48-58)
  • completion (203-244)
holmes/core/tracing.py (1)
  • completion (182-183)
tests/llm/utils/braintrust.py (1)
  • get_experiment_name (166-171)
tests/llm/utils/classifiers.py (1)
  • create_llm_client (16-44)
🪛 Ruff (0.12.2)
tests/llm/conftest.py

521-524: Use ternary operator status = "[green]✓[/green]" if "PASS" in result.pass_fail else "[red]✗[/red]" instead of if-else-block

Replace if-else-block with status = "[green]✓[/green]" if "PASS" in result.pass_fail else "[red]✗[/red]"

(SIM108)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: llm_evals
  • GitHub Check: Pre-commit checks
  • GitHub Check: Pre-commit checks
🔇 Additional comments (9)
tests/llm/conftest.py (9)

1-18: LGTM! Well-organized imports and dependencies.

The import structure is clean and logically organized into standard library, third-party, and local imports. The addition of Rich, LiteLLM, and other dependencies supports the enhanced reporting functionality.


24-38: LGTM! Comprehensive TestResult dataclass design.

The dataclass effectively encapsulates all necessary test metadata with appropriate defaults and optional fields. The structure supports both basic test information and enhanced analysis features.


40-44: Good addition of LiteLLM logging configuration.

Suppressing noisy LiteLLM logs during testing improves the developer experience by reducing log noise.


194-247: Excellent refactoring of terminal summary logic.

The new approach of collecting results from terminalreporter.stats is much cleaner and more pytest-native than the previous Braintrust API approach. The separation of concerns with helper functions makes the code maintainable.


312-334: LGTM! Clean Braintrust URL generation with proper fallbacks.

The function handles missing API keys gracefully and constructs URLs with appropriate defaults for organization names.


336-416: Excellent implementation of regression detection logic.

The distinction between known failures (expected_score = 0, actual_score = 0) and actual regressions is well-implemented. The sorting logic ensures consistent test ordering in reports.


431-465: LGTM! Robust utility functions with proper error handling.

The test ID and name extraction functions include appropriate try-catch blocks and fallback logic for malformed nodeids.


484-542: Excellent Rich table implementation with proper formatting.

The table design with text wrapping, color coding, and proper column sizing enhances the developer experience significantly. The integration with LLM analysis provides valuable insights.


557-593: No additional LLM error-handling or API-key checks needed

The _get_llm_analysis helper already wraps the litellm.completion call in a broad try/except that returns a clear failure message, and the existing create_llm_client in tests/llm/utils/classifiers.py enforces presence of AZURE_API_KEY/OPENAI_API_KEY (and AZURE_API_BASE/ AZURE_API_VERSION). Other LLM calls in holmes/core/tool_calling_llm.py likewise catch and log exceptions.

No further validation or exception branches are required here.

Comment thread tests/llm/conftest.py

@arikalon1 arikalon1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@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)
tests/llm/conftest.py (3)

250-255: Add error handling for score conversions

The float conversions could raise ValueError if the user properties contain invalid data. This addresses a previous review comment that remains valid.

-            expected_correctness_score = float(
-                user_props.get("expected_correctness_score", 1.0)
-            )
-            actual_correctness_score = float(
-                user_props.get("actual_correctness_score", 0.0)
-            )
+            try:
+                expected_correctness_score = float(
+                    user_props.get("expected_correctness_score", 1.0)
+                )
+                actual_correctness_score = float(
+                    user_props.get("actual_correctness_score", 0.0)
+                )
+            except (ValueError, TypeError):
+                # Skip test if scores are invalid
+                continue

389-396: Add error handling for file operations

File write operations should include error handling to prevent crashes during report generation. This addresses a previous review comment that remains valid.

-        with open("evals_report.txt", "w", encoding="utf-8") as file:
-            file.write(markdown)
-
-        # Write regressions file if needed
-        if total_regressions > 0:
-            with open("regressions.txt", "w", encoding="utf-8") as file:
-                file.write(f"{total_regressions}")
+        try:
+            with open("evals_report.txt", "w", encoding="utf-8") as file:
+                file.write(markdown)
+        except IOError as e:
+            print(f"Failed to write evals report: {e}")
+
+        # Write regressions file if needed
+        if total_regressions > 0:
+            try:
+                with open("regressions.txt", "w", encoding="utf-8") as file:
+                    file.write(f"{total_regressions}")
+            except IOError as e:
+                print(f"Failed to write regressions file: {e}")

577-585: Improve error handling specificity for LLM calls

The current broad exception handling could mask important errors. This addresses a previous review comment that remains valid.

-    try:
-        response = completion(
-            model="gpt-4o",
-            messages=[{"role": "user", "content": prompt}],
-            max_tokens=200,
-            temperature=0.1,
-        )
-        return response.choices[0].message.content.strip()
-    except Exception as e:
-        return f"Analysis failed: {e}"
+    try:
+        response = completion(
+            model="gpt-4o",
+            messages=[{"role": "user", "content": prompt}],
+            max_tokens=200,
+            temperature=0.1,
+        )
+        if response.choices and response.choices[0].message.content:
+            return response.choices[0].message.content.strip()
+        else:
+            return "Analysis failed: Empty response from LLM"
+    except ImportError as e:
+        return f"Analysis failed: LiteLLM import error - {e}"
+    except Exception as e:
+        return f"Analysis failed: {type(e).__name__}: {e}"
🧹 Nitpick comments (1)
tests/llm/conftest.py (1)

498-501: Consider using ternary operator for cleaner code

The static analysis tool suggests a valid simplification that would improve readability.

-        # Convert pass/fail to check/x status with colors
-        if "PASS" in pass_fail:
-            status = "[green]✓[/green]"
-        else:
-            status = "[red]✗[/red]"
+        # Convert pass/fail to check/x status with colors  
+        status = "[green]✓[/green]" if "PASS" in pass_fail else "[red]✗[/red]"
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 325a04e and 9cdf045.

📒 Files selected for processing (1)
  • tests/llm/conftest.py (2 hunks)
🧠 Learnings (1)
tests/llm/conftest.py (3)

Learnt from: nherment
PR: #610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025-07-08T08:45:41.069Z
Learning: When suggesting improvements to environment variable handling in robusta-dev/holmesgpt, check first if validation logic already exists rather than reimplementing it.

Learnt from: Sheeproid
PR: #586
File: tests/llm/fixtures/test_ask_holmes/03_what_is_the_command_to_port_forward/test_case.yaml:4-4
Timestamp: 2025-07-02T10:27:17.231Z
Learning: In LLM-as-judge test cases for HolmesGPT, expected outputs should be descriptive rather than prescriptive when testing for flexible responses like port numbers. Using specific values in expected outputs can cause unnecessary test failures when the AI generates different but equally valid responses.

Learnt from: nherment
PR: #610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025-07-08T08:45:41.069Z
Learning: The robusta-dev/holmesgpt codebase has comprehensive existing validation for Azure environment variables (AZURE_API_BASE, AZURE_API_KEY, AZURE_API_VERSION) and MODEL in tests/llm/utils/classifiers.py, tests/llm/conftest.py, and holmes/core/llm.py. Don't suggest adding redundant validation logic.

🧬 Code Graph Analysis (1)
tests/llm/conftest.py (2)
holmes/core/llm.py (2)
  • completion (48-58)
  • completion (203-244)
tests/llm/utils/braintrust.py (1)
  • get_experiment_name (166-171)
🪛 Ruff (0.12.2)
tests/llm/conftest.py

498-501: Use ternary operator status = "[green]✓[/green]" if "PASS" in pass_fail else "[red]✗[/red]" instead of if-else-block

Replace if-else-block with status = "[green]✓[/green]" if "PASS" in pass_fail else "[red]✗[/red]"

(SIM108)

🧰 Additional context used
🧠 Learnings (1)
tests/llm/conftest.py (3)

Learnt from: nherment
PR: #610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025-07-08T08:45:41.069Z
Learning: When suggesting improvements to environment variable handling in robusta-dev/holmesgpt, check first if validation logic already exists rather than reimplementing it.

Learnt from: Sheeproid
PR: #586
File: tests/llm/fixtures/test_ask_holmes/03_what_is_the_command_to_port_forward/test_case.yaml:4-4
Timestamp: 2025-07-02T10:27:17.231Z
Learning: In LLM-as-judge test cases for HolmesGPT, expected outputs should be descriptive rather than prescriptive when testing for flexible responses like port numbers. Using specific values in expected outputs can cause unnecessary test failures when the AI generates different but equally valid responses.

Learnt from: nherment
PR: #610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025-07-08T08:45:41.069Z
Learning: The robusta-dev/holmesgpt codebase has comprehensive existing validation for Azure environment variables (AZURE_API_BASE, AZURE_API_KEY, AZURE_API_VERSION) and MODEL in tests/llm/utils/classifiers.py, tests/llm/conftest.py, and holmes/core/llm.py. Don't suggest adding redundant validation logic.

🧬 Code Graph Analysis (1)
tests/llm/conftest.py (2)
holmes/core/llm.py (2)
  • completion (48-58)
  • completion (203-244)
tests/llm/utils/braintrust.py (1)
  • get_experiment_name (166-171)
🪛 Ruff (0.12.2)
tests/llm/conftest.py

498-501: Use ternary operator status = "[green]✓[/green]" if "PASS" in pass_fail else "[red]✗[/red]" instead of if-else-block

Replace if-else-block with status = "[green]✓[/green]" if "PASS" in pass_fail else "[red]✗[/red]"

(SIM108)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: llm_evals
  • GitHub Check: Pre-commit checks
  • GitHub Check: Pre-commit checks
🔇 Additional comments (5)
tests/llm/conftest.py (5)

1-17: LGTM: Clean imports and structure

The imports are well-organized into standard library, third-party, and local imports. The addition of litellm, rich, and textwrap dependencies aligns with the new functionality for LLM analysis and enhanced console output.


24-38: Good dataclass design for structured test results

The TestResult dataclass provides a clean structure for encapsulating test metadata with appropriate defaults and type hints.


40-44: Appropriate logging configuration for third-party libraries

Suppressing noisy LiteLLM logs during testing is a good practice to keep test output clean and focused.


186-187: Good use of environment variable with fallback

The code correctly uses BRAINTRUST_ORG environment variable with a sensible default fallback to "robustadev".


194-210: Well-structured terminal summary hook

The refactored pytest_terminal_summary function has a clean separation of concerns, delegating to focused helper functions for different output types.

@aantn
aantn merged commit 4384ee8 into master Jul 22, 2025
@aantn
aantn deleted the show-evals-summary branch July 22, 2025 07:39
@github-actions

Copy link
Copy Markdown
Contributor

Results of HolmesGPT evals

  • ask_holmes: 46/66 test cases were successful, 1 regressions
  • investigate: 14/16 test cases were successful, 0 regressions
Test suite Test case Status
ask 01: how_many_pods ✅
ask 02: what_is_wrong_with_pod ✅
ask 03: what_is_the_command_to_port_forward ✅
ask 04: related_k8s_events ✅
ask 05: image_version ✅
ask 06: explain_issue ✅
ask 07: high_latency ✅
ask 08: sock_shop_frontend ⚠️
ask 09: crashpod ✅
ask 10: image_pull_backoff ✅
ask 11: init_containers ✅
ask 12: job_crashing ✅
ask 13: pending_node_selector ✅
ask 14: pending_resources ✅
ask 15: failed_readiness_probe ✅
ask 16: failed_no_toolset_found ✅
ask 17: oom_kill ✅
ask 18: crash_looping_v2 ✅
ask 19: detect_missing_app_details ✅
ask 20: long_log_file_search ✅
ask 21: job_fail_curl_no_svc_account ✅
ask 22: high_latency_dbi_down ⚠️
ask 23: app_error_in_current_logs ✅
ask 24: misconfigured_pvc ✅
ask 25: misconfigured_ingress_class ✅
ask 26: multi_container_logs ✅
ask 27: permissions_error_no_helm_tools ❌
ask 28: permissions_error_helm_tools_enabled ⚠️
ask 29: events_from_alert_manager ✅
ask 30: basic_promql_graph_cluster_memory ✅
ask 31: basic_promql_graph_pod_memory ✅
ask 32: basic_promql_graph_pod_cpu ✅
ask 33: http_latency_graph ✅
ask 34: memory_graph ✅
ask 35: tempo ✅
ask 36: argocd_find_resource ✅
ask 37: argocd_wrong_namespace ⚠️
ask 38: rabbitmq_split_head ✅
ask 39: failed_toolset ✅
ask 40: disabled_toolset ✅
ask 41: setup_argo ⚠️
ask 42: dns_issues_result_all_tools ⚠️
ask 42: dns_issues_result_new_tools ⚠️
ask 42: dns_issues_result_old_tools ⚠️
ask 42: dns_issues_steps_new_all_tools ⚠️
ask 42: dns_issues_steps_new_tools ⚠️
ask 42: dns_issues_steps_old_tools ⚠️
ask 43: current_datetime_from_prompt ✅
ask 43: slack_deployment_logs ✅
ask 44: slack_statefulset_logs ⚠️
ask 45: fetch_deployment_logs_simple ✅
ask 46: job_crashing_no_longer_exists ⚠️
ask 47: truncated_logs_context_window ⚠️
ask 48: logs_since_thursday ⚠️
ask 49: logs_since_last_week ⚠️
ask 50: logs_since_specific_date ⚠️
ask 51: logs_summarize_errors ✅
ask 52: logs_login_issues ✅
ask 53: logs_find_term ✅
ask 54: not_truncated_when_getting_pods ✅
ask 55: kafka_runbook ⚠️
ask 56: kafka_runbook_no_tool ✅
ask 58: counting_pods_by_status ⚠️
ask 59: label_based_counting ✅
ask 60: time_based_filtering ✅
ask 61: exact_match_counting ✅
investigate 01: oom_kill ✅
investigate 02: crashloop_backoff ✅
investigate 03: cpu_throttling ✅
investigate 04: image_pull_backoff ✅
investigate 05: crashpod ✅
investigate 06: job_failure ✅
investigate 07: job_syntax_error ✅
investigate 08: memory_pressure ✅
investigate 09: high_latency ✅
investigate 10: KubeDeploymentReplicasMismatch ✅
investigate 11: KubePodCrashLooping ✅
investigate 12: KubePodNotReady ✅
investigate 13: Watchdog ✅
investigate 14: tempo ✅
investigate 15: dns_resolution ⚠️
investigate 16: dns_resolution_no_tool ⚠️

Legend

  • ✅ the test was successful
  • ⚠️ the test failed but is known to be flaky or known to fail
  • ❌ the test failed and should be fixed before merging the PR

@coderabbitai coderabbitai Bot mentioned this pull request Oct 4, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Jan 21, 2026
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.

2 participants