diff --git a/docs/docs/tools/improve.md b/docs/docs/tools/improve.md index afbb9adcf4..c46ca737ef 100644 --- a/docs/docs/tools/improve.md +++ b/docs/docs/tools/improve.md @@ -60,9 +60,9 @@ num_code_suggestions_per_chunk = ... ### Table vs Committable code comments -PR-Agent supports two modes for presenting code suggestions: +PR-Agent supports two modes for presenting code suggestions: -1) [Table](https://codium.ai/images/pr_agent/code_suggestions_as_comment_closed.png) mode +1) [Table](https://codium.ai/images/pr_agent/code_suggestions_as_comment_closed.png) mode 2) [Inline Committable](https://codium.ai/images/pr_agent/improve.png) code comments mode. @@ -74,7 +74,7 @@ The table format offers several key advantages: - **Centralized tracking**: Shows suggestion implementation status in one place - **IDE integration**: Allows applying suggestions directly in your IDE via the CLI tool -Table mode is the default of PR-Agent, and is recommended approach for most users due to these benefits. +Table mode is the default of PR-Agent, and is recommended approach for most users due to these benefits. ![code_suggestions_as_comment_closed.png](https://codium.ai/images/pr_agent/code_suggestions_as_comment_closed.png){width=512} @@ -113,9 +113,9 @@ Use triple quotes to write multi-line instructions. Use bullet points or numbers PR-Agent supports both simple and hierarchical best practices configurations to provide guidance to the AI model for generating relevant code suggestions. ???- tip "Writing effective best practices files" - + The following guidelines apply to all best practices files: - + - Write clearly and concisely - Include brief code examples when helpful with before/after patterns - Focus on project-specific guidelines that will result in relevant suggestions you actually want to get @@ -126,9 +126,9 @@ PR-Agent supports both simple and hierarchical best practices configurations to - Use pattern-based structure rather than simple bullet points for better clarity ???- tip "Example of a best practices file" - + Pattern 1: Add proper error handling with try-except blocks around external function calls. - + Example code before: ```python @@ -147,7 +147,7 @@ PR-Agent supports both simple and hierarchical best practices configurations to ``` Pattern 2: Add defensive null/empty checks before accessing object properties or performing operations on potentially null variables to prevent runtime errors. - + Example code before: ```python @@ -315,7 +315,7 @@ Note: Chunking is primarily relevant for large PRs. For most PRs (up to 600 line focus_only_on_problems - If set to true, suggestions will focus primarily on identifying and fixing code problems, and less on style considerations like best practices, maintainability, or readability. Default is true. + If set to true, suggestions will focus primarily on identifying and fixing code problems, and less on style considerations like best practices, maintainability, or readability. Default is true. persistent_comment @@ -337,6 +337,13 @@ Note: Chunking is primarily relevant for large PRs. For most PRs (up to 600 line publish_output_no_suggestions If set to true, the tool will publish a comment even if no suggestions were found. Default is true. + + enable_suggestions_coverage_footer + + If set to true, the tool will display a coverage notice when failed analysis chunks make the + suggestions incomplete. Default is true. + + ???+ example "Params for number of suggestions and AI calls" diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 5a321620d1..3b3d2b868f 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -1,13 +1,14 @@ -from abc import ABC, abstractmethod # enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED) import os import shutil import subprocess import time +from abc import ABC, abstractmethod from typing import Optional, Tuple from pr_agent.algo.types import FilePatchInfo -from pr_agent.algo.utils import Range, add_pr_review_identity, comment_matches_identity, process_description +from pr_agent.algo.utils import (Range, add_pr_review_identity, + comment_matches_identity, process_description) from pr_agent.config_loader import get_settings from pr_agent.log import get_logger @@ -130,6 +131,17 @@ def supports_code_suggestions_artifact(self) -> bool: """Return whether `publish_code_suggestions()` writes a standalone output artifact.""" return False + def publish_code_suggestions_artifact( + self, code_suggestions: list, artifact_footer: str = "", + no_suggestions_message: str = "No code suggestions found for the PR.") -> bool: + """Publish suggestions to a standalone artifact, optionally with additional context. + + Providers that return True from `supports_code_suggestions_artifact()` should override + this method when they can preserve the footer in the same artifact. The default keeps + backward compatibility for providers that only implement `publish_code_suggestions()`. + """ + return self.publish_code_suggestions(code_suggestions) + #Given a url (issues or PR/MR) - get the .git repo url to which they belong. Needs to be implemented by the provider. def get_git_repo_url(self, issues_or_pr_url: str) -> str: get_logger().warning("Not implemented! Returning empty url") diff --git a/pr_agent/git_providers/local_git_provider.py b/pr_agent/git_providers/local_git_provider.py index 310b3e19e6..5795c8b5fb 100644 --- a/pr_agent/git_providers/local_git_provider.py +++ b/pr_agent/git_providers/local_git_provider.py @@ -145,6 +145,11 @@ def publish_code_suggestion(self, body: str, relevant_file: str, raise NotImplementedError('Publishing code suggestions is not implemented for the local git provider') def publish_code_suggestions(self, code_suggestions: list) -> bool: + return self.publish_code_suggestions_artifact(code_suggestions) + + def publish_code_suggestions_artifact( + self, code_suggestions: list, artifact_footer: str = "", + no_suggestions_message: str = "No code suggestions found for the PR.") -> bool: """ Write /improve output to a file (improve.md by default). @@ -167,7 +172,8 @@ def publish_code_suggestions(self, code_suggestions: list) -> bool: sections.append(f"{header}\n\n{suggestion.get('body', '').strip()}") header = format_pr_code_suggestions_header(markdown_level=1) pr_body = f"{header}\n\n" + "\n\n".join(sections) if sections \ - else f"{header}\n\nNo code suggestions found for the PR." + else f"{header}\n\n{no_suggestions_message}" + pr_body += artifact_footer if not sections and get_settings().get("config.output_run_details", False): pr_body += show_run_details(False) with open(self.improve_path, "w", encoding="utf-8") as file: diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index 4d133183e9..e454f196b2 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -179,6 +179,7 @@ enable_chat_text=false persistent_comment=true max_history_len=4 publish_output_no_suggestions=true +enable_suggestions_coverage_footer=true # show when failed analysis chunks make the suggestions incomplete # suggestions scoring suggestions_score_threshold=0 # [0-10]| recommend not to set this value above 8, since above it may clip highly relevant suggestions new_score_mechanism=true diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index a45dd9df18..f253eda818 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -30,7 +30,8 @@ format_pr_code_suggestions_header, get_max_tokens, get_model, load_yaml, replace_code_tags, - show_relevant_configurations, show_run_details) + show_relevant_configurations, + show_run_details) from pr_agent.config_loader import get_settings from pr_agent.git_providers import (AzureDevopsProvider, GithubProvider, GitLabProvider, get_git_provider, @@ -207,6 +208,7 @@ async def run(self): # generate summarized suggestions pr_body = self.generate_summarized_suggestions(data) + pr_body += self._get_suggestions_coverage_footer() get_logger().debug(f"PR output", artifact=pr_body) # require self-review @@ -266,6 +268,7 @@ async def run(self): else: get_logger().info('Code suggestions generated for PR, but not published since publish_output is False.') pr_body = self.generate_summarized_suggestions(data) + pr_body += self._get_suggestions_coverage_footer() get_settings().data = {"artifact": pr_body} return except Exception as e: @@ -296,13 +299,30 @@ async def add_self_review_text(self, pr_body): pr_body += ' ' return pr_body + def _get_suggestions_coverage_footer(self, suggestions_present: bool = True) -> str: + failed_chunk_count = getattr(self, "failed_chunk_count", 0) + if (not failed_chunk_count or + not get_settings().pr_code_suggestions.get("enable_suggestions_coverage_footer", True)): + return "" + total_chunk_count = getattr(self, "total_chunk_count", failed_chunk_count) + coverage_detail = ("the suggestions above are based on the successful chunks only." + if suggestions_present else + "no suggestions were found in the successful chunks; failed chunks could not be analyzed.") + return (f"\n\n⚠️ **Suggestion coverage:** {failed_chunk_count} of {total_chunk_count} " + "analysis chunks failed; " + f"{coverage_detail}") + async def publish_no_suggestions(self): - pr_body = f"{format_pr_code_suggestions_header()}\n\nNo code suggestions found for the PR." + coverage_footer = self._get_suggestions_coverage_footer(suggestions_present=False) + no_suggestions_message = ("No code suggestions found in the successfully analyzed chunks." + if coverage_footer else "No code suggestions found for the PR.") + pr_body = f"{format_pr_code_suggestions_header()}\n\n{no_suggestions_message}{coverage_footer}" if (get_settings().config.publish_output and get_settings().pr_code_suggestions.get('publish_output_no_suggestions', True)): get_logger().warning("No code suggestions found for the PR.") - if self.git_provider.supports_code_suggestions_artifact(): - self.git_provider.publish_code_suggestions([]) + if self.git_provider.supports_code_suggestions_artifact() is True: + self.git_provider.publish_code_suggestions_artifact( + [], artifact_footer=coverage_footer, no_suggestions_message=no_suggestions_message) return pr_body = add_comment_identity( pr_body, @@ -318,7 +338,7 @@ async def publish_no_suggestions(self): else: self.git_provider.publish_comment(pr_body) else: - get_settings().data = {"artifact": ""} + get_settings().data = {"artifact": pr_body if coverage_footer else ""} if self.progress_response: self.git_provider.remove_comment(self.progress_response) @@ -337,7 +357,7 @@ async def dual_publishing(self, data): if data_above_threshold['code_suggestions']: get_logger().info( f"Publishing {len(data_above_threshold['code_suggestions'])} suggestions in dual publishing mode") - await self.push_inline_code_suggestions(data_above_threshold) + await self.push_inline_code_suggestions(data_above_threshold, include_coverage_footer=False) except Exception as e: get_logger().error(f"Failed to publish dual publishing suggestions, error: {e}") @@ -740,17 +760,24 @@ def _prepare_pr_code_suggestions(self, predictions: str) -> Dict: return data - async def push_inline_code_suggestions(self, data): + async def push_inline_code_suggestions(self, data, include_coverage_footer: bool = True): code_suggestions = [] fallback_comments = [] + coverage_footer = self._get_suggestions_coverage_footer() if include_coverage_footer else "" + supports_suggestions_artifact = self.git_provider.supports_code_suggestions_artifact() is True if not data['code_suggestions']: get_logger().info('No suggestions found to improve this PR.') + empty_coverage_footer = (self._get_suggestions_coverage_footer(suggestions_present=False) + if include_coverage_footer else "") + no_suggestions_message = ("No suggestions found in the successfully analyzed chunks." + if empty_coverage_footer else "No suggestions found to improve this PR.") + pr_body = no_suggestions_message + empty_coverage_footer if self.progress_response: return self.git_provider.edit_comment(self.progress_response, - body='No suggestions found to improve this PR.') + body=pr_body) else: - return self.git_provider.publish_comment('No suggestions found to improve this PR.') + return self.git_provider.publish_comment(pr_body) for d in data['code_suggestions']: try: @@ -796,11 +823,17 @@ async def push_inline_code_suggestions(self, data): 'original_suggestion': d}) if code_suggestions: - is_successful = self.git_provider.publish_code_suggestions(code_suggestions) + if supports_suggestions_artifact: + is_successful = self.git_provider.publish_code_suggestions_artifact( + code_suggestions, artifact_footer=coverage_footer) + else: + is_successful = self.git_provider.publish_code_suggestions(code_suggestions) if not is_successful: get_logger().info("Failed to publish code suggestions, trying to publish each suggestion separately") for code_suggestion in code_suggestions: self.git_provider.publish_code_suggestions([code_suggestion]) + if coverage_footer and not supports_suggestions_artifact: + fallback_comments.append(coverage_footer.strip()) if fallback_comments: self.git_provider.publish_comment("\n\n---\n\n".join(fallback_comments)) @@ -1112,6 +1145,8 @@ def remove_line_numbers(self, patches_diff_list: List[str]) -> List[str]: return patches_diff_list async def prepare_prediction_main(self, model: str) -> dict: + self.failed_chunk_count = 0 + self.total_chunk_count = 0 # get PR diff if get_settings().pr_code_suggestions.decouple_hunks: self.patches_diff_list = get_pr_multi_diffs(self.git_provider, @@ -1145,6 +1180,7 @@ async def prepare_prediction_main(self, model: str) -> dict: prediction_list = [] chunk_errors = [] chunk_pairs = list(zip(self.patches_diff_list, self.patches_diff_list_no_line_numbers)) + self.total_chunk_count = len(chunk_pairs) # parallelize calls to AI: if get_settings().pr_code_suggestions.parallel_calls: @@ -1179,6 +1215,7 @@ async def prepare_prediction_main(self, model: str) -> dict: else: prediction_list.append(prediction) + self.failed_chunk_count = len(chunk_errors) if chunk_errors and not prediction_list: raise chunk_errors[0] self.prediction_list = prediction_list diff --git a/tests/unittest/test_local_git_provider.py b/tests/unittest/test_local_git_provider.py index 1c423e4b6c..1e05efeafb 100644 --- a/tests/unittest/test_local_git_provider.py +++ b/tests/unittest/test_local_git_provider.py @@ -121,6 +121,22 @@ def test_publish_code_suggestions_no_suggestions(tmp_path): assert "No code suggestions found" in improve_path.read_text() +def test_publish_code_suggestions_artifact_includes_partial_coverage(tmp_path): + improve_path = tmp_path / "improve.md" + provider = object.__new__(LocalGitProvider) + provider.improve_path = improve_path + + assert provider.publish_code_suggestions_artifact( + [], + artifact_footer="\n\n⚠️ **Suggestion coverage:** 1 of 2 analysis chunks failed.", + no_suggestions_message="No code suggestions found in the successfully analyzed chunks.", + ) is True + + content = improve_path.read_text() + assert "No code suggestions found in the successfully analyzed chunks." in content + assert "1 of 2 analysis chunks failed" in content + + def test_publish_code_suggestions_uses_custom_heading_without_identity(tmp_path): snapshot = snapshot_settings(["pr_code_suggestions.suggestions_heading"]) improve_path = tmp_path / "improve.md" diff --git a/tests/unittest/test_pr_code_suggestions_core.py b/tests/unittest/test_pr_code_suggestions_core.py index 61a0766164..05e6ad26d0 100644 --- a/tests/unittest/test_pr_code_suggestions_core.py +++ b/tests/unittest/test_pr_code_suggestions_core.py @@ -11,7 +11,8 @@ from pr_agent.config_loader import get_settings from pr_agent.git_providers.git_provider import GitProvider, IncrementalPR from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions -from tests.unittest._settings_helpers import restore_settings, snapshot_settings +from tests.unittest._settings_helpers import (restore_settings, + snapshot_settings) def _make_tool(git_provider=None): @@ -156,6 +157,8 @@ async def fake_get_prediction(model, patches_diff, patches_diff_no_line_numbers) assert calls == ["chunk-a", "chunk-b"] assert successful_chunk_finished.is_set() + assert tool.failed_chunk_count == 1 + assert tool.total_chunk_count == 2 assert data["code_suggestions"] == [_valid_suggestion(relevant_file="chunk-a.py")] @@ -219,6 +222,8 @@ async def fake_get_prediction(model, patches_diff, patches_diff_no_line_numbers) settings.pr_code_suggestions.parallel_calls = original_parallel_calls assert calls == ["chunk-a", "chunk-b", "chunk-c"] + assert tool.failed_chunk_count == 1 + assert tool.total_chunk_count == 3 assert data["code_suggestions"] == [ _valid_suggestion(relevant_file="chunk-a.py"), _valid_suggestion(relevant_file="chunk-c.py"), @@ -266,6 +271,66 @@ async def fake_get_prediction(model, patches_diff, patches_diff_no_line_numbers) ("fallback-model", "chunk-b"), ] assert len(data["code_suggestions"]) == 2 + assert tool.failed_chunk_count == 0 + assert tool.total_chunk_count == 2 + + +def test_suggestions_coverage_footer_reports_partial_runs_and_respects_flag(): + settings = get_settings() + snapshot = snapshot_settings(["pr_code_suggestions.enable_suggestions_coverage_footer"]) + tool = _make_tool() + tool.failed_chunk_count = 1 + tool.total_chunk_count = 3 + + try: + settings.set("pr_code_suggestions.enable_suggestions_coverage_footer", True) + footer = tool._get_suggestions_coverage_footer() + assert "1 of 3 analysis chunks failed" in footer + assert "successful chunks only" in footer + + empty_footer = tool._get_suggestions_coverage_footer(suggestions_present=False) + assert "no suggestions were found in the successful chunks" in empty_footer + assert "failed chunks could not be analyzed" in empty_footer + + settings.set("pr_code_suggestions.enable_suggestions_coverage_footer", False) + assert tool._get_suggestions_coverage_footer() == "" + finally: + restore_settings(snapshot) + + +def test_suggestions_coverage_footer_is_safe_for_tools_built_without_init(): + tool = _make_tool() + + assert tool._get_suggestions_coverage_footer() == "" + + +@pytest.mark.asyncio +async def test_run_appends_partial_suggestions_coverage_to_the_summary(): + snapshot = snapshot_settings([ + "config.publish_output", + "data", + "pr_code_suggestions.enable_suggestions_coverage_footer", + ]) + tool = _make_tool() + tool.pr_url = "https://example.test/pull/1" + tool.git_provider.get_files.return_value = ["app.py"] + tool.generate_summarized_suggestions = MagicMock(return_value="Base suggestions body") + tool.failed_chunk_count = 1 + tool.total_chunk_count = 2 + + try: + get_settings().set("config.publish_output", False) + get_settings().set("pr_code_suggestions.enable_suggestions_coverage_footer", True) + with (patch("pr_agent.tools.pr_code_suggestions.init_run_details"), + patch("pr_agent.tools.pr_code_suggestions.retry_with_fallback_models", + AsyncMock(return_value={"code_suggestions": [_valid_suggestion()]}))): + await tool.run() + + artifact = get_settings().data["artifact"] + assert artifact.startswith("Base suggestions body") + assert "1 of 2 analysis chunks failed" in artifact + finally: + restore_settings(snapshot) def test_dedent_code_matches_target_file_indentation(): @@ -747,6 +812,8 @@ def test_summarized_suggestions_normalize_both_sides_of_the_diff(): async def test_suggestion_covering_the_anchored_range_is_published_as_committable(): git_provider = _provider_with_file("def f():\n return old()\n") tool = _make_tool(git_provider) + tool.failed_chunk_count = 1 + tool.total_chunk_count = 2 await tool.push_inline_code_suggestions({"code_suggestions": [ _valid_suggestion( @@ -889,6 +956,28 @@ async def test_publish_no_suggestions_still_overwrites_the_progress_comment_when git_provider.remove_comment.assert_not_called() +@pytest.mark.asyncio +async def test_publish_no_suggestions_qualifies_partial_results(publish_output_no_suggestions): + publish_output_no_suggestions(True) + snapshot = snapshot_settings(["pr_code_suggestions.enable_suggestions_coverage_footer"]) + git_provider = MagicMock() + git_provider.supports_code_suggestions_artifact.return_value = False + tool = _make_tool(git_provider) + tool.failed_chunk_count = 1 + tool.total_chunk_count = 2 + + try: + get_settings().set("pr_code_suggestions.enable_suggestions_coverage_footer", True) + await tool.publish_no_suggestions() + finally: + restore_settings(snapshot) + + body = git_provider.publish_comment.call_args.args[0] + assert "No code suggestions found in the successfully analyzed chunks." in body + assert "1 of 2 analysis chunks failed" in body + assert "failed chunks could not be analyzed" in body + + @pytest.mark.asyncio async def test_publish_no_suggestions_uses_provider_artifact_capability(publish_output_no_suggestions): publish_output_no_suggestions(True) @@ -898,11 +987,38 @@ async def test_publish_no_suggestions_uses_provider_artifact_capability(publish_ await tool.publish_no_suggestions() - git_provider.publish_code_suggestions.assert_called_once_with([]) + git_provider.publish_code_suggestions_artifact.assert_called_once_with( + [], artifact_footer="", no_suggestions_message="No code suggestions found for the PR.") + git_provider.publish_code_suggestions.assert_not_called() git_provider.publish_comment.assert_not_called() git_provider.edit_comment.assert_not_called() +@pytest.mark.asyncio +async def test_publish_no_suggestions_keeps_partial_notice_in_disabled_output_artifact( + publish_output_no_suggestions): + publish_output_no_suggestions(True) + snapshot = snapshot_settings([ + "config.publish_output", + "data", + "pr_code_suggestions.enable_suggestions_coverage_footer", + ]) + tool = _make_tool() + tool.failed_chunk_count = 1 + tool.total_chunk_count = 2 + + try: + get_settings().set("config.publish_output", False) + get_settings().set("pr_code_suggestions.enable_suggestions_coverage_footer", True) + await tool.publish_no_suggestions() + artifact = get_settings().data["artifact"] + finally: + restore_settings(snapshot) + + assert "No code suggestions found in the successfully analyzed chunks." in artifact + assert "1 of 2 analysis chunks failed" in artifact + + def test_setup_incremental_scope_calls_provider_when_supported(): git_provider = MagicMock() git_provider.supports_incremental_kind.return_value = True @@ -1025,6 +1141,7 @@ async def test_dual_publishing_keeps_suggestions_without_replacement_code(): ]}) assert "Use the shared helper." in _published_suggestion(git_provider)["body"] + git_provider.publish_comment.assert_not_called() finally: settings.set("pr_code_suggestions.dual_publishing_score_threshold", original_threshold) diff --git a/tests/unittest/test_pr_code_suggestions_rendering.py b/tests/unittest/test_pr_code_suggestions_rendering.py index c3eeebc0f3..05978dad0b 100644 --- a/tests/unittest/test_pr_code_suggestions_rendering.py +++ b/tests/unittest/test_pr_code_suggestions_rendering.py @@ -5,7 +5,8 @@ from pr_agent.algo.types import FilePatchInfo from pr_agent.config_loader import get_settings from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions -from tests.unittest._settings_helpers import restore_settings, snapshot_settings +from tests.unittest._settings_helpers import (restore_settings, + snapshot_settings) TRUNCATION_SETTINGS = ( "pr_code_suggestions.max_code_suggestion_length", @@ -132,6 +133,31 @@ async def test_push_inline_renders_body_with_score_and_label(): assert args[0]["original_suggestion"]["one_sentence_summary"] == "Use the shared helper" +@pytest.mark.asyncio +async def test_push_inline_publishes_partial_coverage_notice(): + git_provider = MagicMock() + git_provider.diff_files = [ + FilePatchInfo( + base_file="", + head_file="def f():\n return old()\n", + patch="", + filename="app.py", + ) + ] + git_provider.publish_code_suggestions.return_value = True + git_provider.supports_code_suggestions_artifact.return_value = False + tool = _make_tool(git_provider) + tool.failed_chunk_count = 1 + tool.total_chunk_count = 2 + + await tool.push_inline_code_suggestions({"code_suggestions": [_suggestion()]}) + + git_provider.publish_code_suggestions.assert_called_once() + coverage_comment = git_provider.publish_comment.call_args.args[0] + assert "1 of 2 analysis chunks failed" in coverage_comment + assert "successful chunks only" in coverage_comment + + @pytest.mark.asyncio async def test_push_inline_renders_body_without_score_when_missing_or_zero(): git_provider = MagicMock() @@ -169,6 +195,21 @@ async def test_push_inline_publishes_no_suggestions_comment_when_empty(): git_provider.publish_code_suggestions.assert_not_called() +@pytest.mark.asyncio +async def test_push_inline_qualifies_empty_partial_results(): + git_provider = MagicMock() + tool = _make_tool(git_provider) + tool.failed_chunk_count = 1 + tool.total_chunk_count = 3 + + await tool.push_inline_code_suggestions({"code_suggestions": []}) + + body = git_provider.publish_comment.call_args.args[0] + assert "successfully analyzed chunks" in body + assert "1 of 3 analysis chunks failed" in body + assert "could not be analyzed" in body + + # --------------------------------------------------------------------------- # generate_summarized_suggestions # ---------------------------------------------------------------------------