Skip to content

Commit 4765936

Browse files
julienldclaude
andauthored
feat: improve ha_report_issue with title, duplicate check, and markdown formatting (#484)
* feat: improve ha_report_issue with title, duplicate check, and markdown formatting - Add automatic title generation based on error messages or connection status - Add duplicate check URLs for searching similar issues before submission - Update instructions to guide agents to present reports in markdown code blocks - Add search keywords generation for duplicate detection - Update tests to verify new features Closes #467 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: address Gemini code review feedback - Use direct log iteration in _generate_search_keywords (fixes bug in string parsing) - Use urllib.parse.quote_plus for proper URL encoding - Define URL constants to avoid duplication across templates and instructions - Refactor title truncation logic to avoid duplication - Add test fixture for unwrapping decorated function - Make test assertions more specific and verify search keywords in URLs Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent f21c431 commit 4765936

2 files changed

Lines changed: 249 additions & 16 deletions

File tree

src/ha_mcp/tools/tools_bug_report.py

Lines changed: 123 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import sys
1212
from pathlib import Path
1313
from typing import Annotated, Any
14+
from urllib.parse import quote_plus
1415

1516
from pydantic import Field
1617

@@ -21,6 +22,10 @@
2122

2223
logger = logging.getLogger(__name__)
2324

25+
# GitHub issue template URLs
26+
RUNTIME_BUG_URL = "https://github.com/homeassistant-ai/ha-mcp/issues/new?template=runtime_bug.md"
27+
AGENT_BEHAVIOR_URL = "https://github.com/homeassistant-ai/ha-mcp/issues/new?template=agent_behavior_feedback.md"
28+
2429

2530
def _detect_installation_method() -> str:
2631
"""
@@ -226,6 +231,16 @@ async def ha_report_issue(
226231
# Anonymization instructions
227232
anonymization_guide = _generate_anonymization_guide()
228233

234+
# Generate suggested title
235+
suggested_title = _generate_bug_title(diagnostic_info, recent_logs)
236+
237+
# Generate search keywords and URLs for duplicate check
238+
search_keywords = _generate_search_keywords(diagnostic_info, recent_logs)
239+
duplicate_check_urls = [
240+
f"https://github.com/homeassistant-ai/ha-mcp/issues?q=is%3Aissue+{quote_plus(keyword)}"
241+
for keyword in search_keywords[:3] # Limit to top 3 keywords
242+
]
243+
229244
return {
230245
"success": True,
231246
"diagnostic_info": diagnostic_info,
@@ -237,21 +252,35 @@ async def ha_report_issue(
237252
"runtime_bug_template": runtime_bug_template,
238253
"agent_behavior_template": agent_behavior_template,
239254
"anonymization_guide": anonymization_guide,
255+
"suggested_title": suggested_title,
256+
"duplicate_check_urls": duplicate_check_urls,
240257
"instructions": (
241-
"ANALYZE THE CONVERSATION to determine which template to present:\n\n"
242-
"🐛 Present RUNTIME_BUG_TEMPLATE if:\n"
243-
" - User reports an error, failure, or unexpected behavior in ha-mcp\n"
244-
" - A tool returned an error or incorrect result\n"
245-
" - Something is broken or not working\n"
246-
" Submit at: https://github.com/homeassistant-ai/ha-mcp/issues/new?template=runtime_bug.md\n\n"
247-
"🤖 Present AGENT_BEHAVIOR_TEMPLATE if:\n"
248-
" - User mentions YOU (the agent) used the wrong tool\n"
249-
" - User suggests YOU should have done something differently\n"
250-
" - User reports YOUR inefficiency or mistakes\n"
251-
" Submit at: https://github.com/homeassistant-ai/ha-mcp/issues/new?template=agent_behavior_feedback.md\n\n"
252-
"If UNCLEAR which type, ASK: 'Are you reporting a bug in ha-mcp, or providing feedback on how I used the tools?'\n\n"
253-
"Present the chosen template to the user. Ask them to fill in the description sections. "
254-
"Remind them to follow the anonymization_guide to protect their privacy."
258+
"WORKFLOW FOR PRESENTING BUG REPORTS:\n\n"
259+
"1. **Check for duplicates FIRST** (before presenting the template):\n"
260+
" - Use the duplicate_check_urls to search for similar issues\n"
261+
" - If gh CLI is available: use `gh issue list --search \"keyword\"`\n"
262+
" - Otherwise: inform user to check the duplicate_check_urls\n"
263+
" - If duplicates found, ask user if they want to comment on existing issue instead\n\n"
264+
"2. **Determine which template to present**:\n"
265+
" - ANALYZE THE CONVERSATION to determine which template to present\n\n"
266+
" 🐛 Present RUNTIME_BUG_TEMPLATE if:\n"
267+
" - User reports an error, failure, or unexpected behavior in ha-mcp\n"
268+
" - A tool returned an error or incorrect result\n"
269+
" - Something is broken or not working\n\n"
270+
" 🤖 Present AGENT_BEHAVIOR_TEMPLATE if:\n"
271+
" - User mentions YOU (the agent) used the wrong tool\n"
272+
" - User suggests YOU should have done something differently\n"
273+
" - User reports YOUR inefficiency or mistakes\n\n"
274+
" If UNCLEAR which type, ASK: 'Are you reporting a bug in ha-mcp, or providing feedback on how I used the tools?'\n\n"
275+
"3. **Present the report to the user**:\n"
276+
" a. Show the suggested_title (user can edit if needed)\n"
277+
" b. Present the chosen template IN A MARKDOWN CODE BLOCK (```markdown...```) for easy copy/paste\n"
278+
" c. PROMINENTLY display the submission URL at the top:\n"
279+
f" - Runtime bugs: {RUNTIME_BUG_URL}\n"
280+
f" - Agent behavior: {AGENT_BEHAVIOR_URL}\n"
281+
" d. Ask them to fill in the description sections\n"
282+
" e. Remind them to follow the anonymization_guide to protect their privacy\n\n"
283+
"CRITICAL: Always present templates in markdown code blocks (```markdown...```) so users can copy/paste easily!"
255284
),
256285
}
257286

@@ -322,6 +351,84 @@ def _extract_error_messages(logs: list[dict[str, Any]]) -> list[str]:
322351
return error_messages
323352

324353

354+
def _generate_bug_title(
355+
diagnostic_info: dict[str, Any],
356+
recent_logs: list[dict[str, Any]],
357+
) -> str:
358+
"""
359+
Generate a concise bug title (single line, ~60 chars max).
360+
361+
Strategy:
362+
1. If there are error messages, use the most recent one as basis
363+
2. Otherwise, use generic template based on connection status
364+
3. Truncate to ~60 chars max
365+
"""
366+
title = ""
367+
# Try to get the most recent error directly from logs
368+
for log in reversed(recent_logs):
369+
error_msg = log.get("error_message")
370+
if error_msg:
371+
tool_name = log.get("tool_name", "unknown")
372+
title = f"{tool_name}: {error_msg}"
373+
break
374+
375+
if not title:
376+
# No errors - check connection status
377+
conn_status = diagnostic_info.get("connection_status", "Unknown")
378+
if "Error" in conn_status or "Failed" in conn_status:
379+
title = f"Connection issue: {conn_status}"
380+
else:
381+
title = "Issue with ha-mcp"
382+
383+
# Truncate to ~60 chars, trying to preserve words
384+
if len(title) > 60:
385+
title = title[:57] + "..."
386+
387+
return title
388+
389+
390+
def _generate_search_keywords(
391+
diagnostic_info: dict[str, Any],
392+
recent_logs: list[dict[str, Any]],
393+
) -> list[str]:
394+
"""
395+
Generate search keywords for duplicate issue detection.
396+
397+
Returns a list of keywords to search for similar issues.
398+
"""
399+
keywords = set()
400+
401+
# Find the most recent error from logs
402+
last_error_log = next((log for log in reversed(recent_logs) if log.get("error_message")), None)
403+
404+
if last_error_log:
405+
tool_name = last_error_log.get("tool_name")
406+
if tool_name:
407+
keywords.add(tool_name)
408+
409+
error_msg = last_error_log.get("error_message", "").lower()
410+
# Common error patterns
411+
if "connection" in error_msg:
412+
keywords.add("connection")
413+
if "timeout" in error_msg:
414+
keywords.add("timeout")
415+
if "authentication" in error_msg or "auth" in error_msg:
416+
keywords.add("authentication")
417+
if "not found" in error_msg:
418+
keywords.add("not found")
419+
420+
# Add connection-based keywords
421+
conn_status = diagnostic_info.get("connection_status", "Unknown")
422+
if "Error" in conn_status or "Failed" in conn_status:
423+
keywords.add("connection")
424+
425+
# Default to generic search if no specific keywords
426+
if not keywords:
427+
keywords.add("bug")
428+
429+
return list(keywords)
430+
431+
325432
def _generate_runtime_bug_template(
326433
diagnostic_info: dict[str, Any],
327434
log_summary: str,
@@ -365,7 +472,7 @@ def _generate_runtime_bug_template(
365472
> All environment info and logs below were collected automatically.
366473
367474
**Submit this report at:**
368-
https://github.com/homeassistant-ai/ha-mcp/issues/new?template=runtime_bug.md
475+
{RUNTIME_BUG_URL}
369476
370477
---
371478
@@ -456,7 +563,7 @@ def _generate_agent_behavior_template(
456563
> Tool call history was collected automatically to help analyze agent behavior.
457564
458565
**Submit this feedback at:**
459-
https://github.com/homeassistant-ai/ha-mcp/issues/new?template=agent_behavior_feedback.md
566+
{AGENT_BEHAVIOR_URL}
460567
461568
---
462569

tests/src/unit/test_tools_bug_report.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ def registered_tools(self, mock_mcp, mock_client):
4242
register_bug_report_tools(mock_mcp, mock_client)
4343
return mock_mcp
4444

45+
@pytest.fixture
46+
def ha_report_issue_func(self, registered_tools):
47+
"""Return the unwrapped ha_report_issue function."""
48+
ha_report_issue = registered_tools._tools["ha_report_issue"]
49+
actual_func = ha_report_issue
50+
while hasattr(actual_func, "__wrapped__"):
51+
actual_func = actual_func.__wrapped__
52+
return actual_func
53+
4554
@pytest.mark.asyncio
4655
async def test_bug_report_success(self, registered_tools, mock_client):
4756
"""Test successful bug report generation."""
@@ -77,6 +86,8 @@ async def test_bug_report_success(self, registered_tools, mock_client):
7786
assert "recent_logs" in result
7887
assert "log_count" in result
7988
assert "instructions" in result
89+
assert "suggested_title" in result
90+
assert "duplicate_check_urls" in result
8091

8192
# Check diagnostic info
8293
diag = result["diagnostic_info"]
@@ -314,3 +325,118 @@ async def test_bug_report_instructions(self, registered_tools, mock_client):
314325
assert "AGENT_BEHAVIOR_TEMPLATE" in instructions
315326
assert "ANALYZE THE CONVERSATION" in instructions
316327
assert "privacy" in instructions.lower()
328+
329+
@pytest.mark.asyncio
330+
async def test_bug_report_suggested_title(self, ha_report_issue_func, mock_client):
331+
"""Test that a suggested title is generated."""
332+
mock_client.get_config.return_value = {"version": "2024.12.0"}
333+
mock_client.get_states.return_value = []
334+
335+
with patch(
336+
"ha_mcp.tools.tools_bug_report.get_recent_logs"
337+
) as mock_get_logs:
338+
mock_get_logs.return_value = [
339+
{
340+
"timestamp": "2024-12-01T10:00:00",
341+
"tool_name": "ha_call_service",
342+
"success": False,
343+
"execution_time_ms": 50,
344+
"error_message": "Service not found",
345+
},
346+
]
347+
348+
result = await ha_report_issue_func()
349+
350+
# Check suggested title is present and has correct format
351+
assert "suggested_title" in result
352+
title = result["suggested_title"]
353+
assert isinstance(title, str)
354+
assert len(title) > 0
355+
assert len(title) <= 60
356+
# Should be exactly: "tool_name: error_message"
357+
assert title == "ha_call_service: Service not found"
358+
359+
@pytest.mark.asyncio
360+
async def test_bug_report_suggested_title_no_errors(
361+
self, ha_report_issue_func, mock_client
362+
):
363+
"""Test title generation when there are no errors."""
364+
mock_client.get_config.return_value = {"version": "2024.12.0"}
365+
mock_client.get_states.return_value = []
366+
367+
with patch(
368+
"ha_mcp.tools.tools_bug_report.get_recent_logs"
369+
) as mock_get_logs:
370+
mock_get_logs.return_value = [
371+
{
372+
"timestamp": "2024-12-01T10:00:00",
373+
"tool_name": "ha_get_state",
374+
"success": True,
375+
"execution_time_ms": 100,
376+
},
377+
]
378+
379+
result = await ha_report_issue_func()
380+
381+
# Should still generate a generic title
382+
assert "suggested_title" in result
383+
title = result["suggested_title"]
384+
assert isinstance(title, str)
385+
assert len(title) > 0
386+
assert len(title) <= 60
387+
388+
@pytest.mark.asyncio
389+
async def test_bug_report_duplicate_check_urls(self, ha_report_issue_func, mock_client):
390+
"""Test that duplicate check URLs are generated with correct keywords."""
391+
mock_client.get_config.return_value = {"version": "2024.12.0"}
392+
mock_client.get_states.return_value = []
393+
394+
with patch(
395+
"ha_mcp.tools.tools_bug_report.get_recent_logs"
396+
) as mock_get_logs:
397+
mock_get_logs.return_value = [
398+
{
399+
"timestamp": "2024-12-01T10:00:00",
400+
"tool_name": "ha_call_service",
401+
"success": False,
402+
"execution_time_ms": 50,
403+
"error_message": "Connection timeout",
404+
},
405+
]
406+
407+
result = await ha_report_issue_func()
408+
409+
# Check duplicate check URLs are present
410+
assert "duplicate_check_urls" in result
411+
urls = result["duplicate_check_urls"]
412+
assert isinstance(urls, list)
413+
assert len(urls) > 0
414+
# All URLs should be GitHub issue search URLs
415+
for url in urls:
416+
assert "github.com/homeassistant-ai/ha-mcp/issues" in url
417+
assert "is%3Aissue" in url
418+
419+
# Verify keywords are in URLs
420+
url_content = "".join(urls)
421+
assert "ha_call_service" in url_content
422+
assert "connection" in url_content
423+
assert "timeout" in url_content
424+
425+
@pytest.mark.asyncio
426+
async def test_bug_report_updated_instructions(
427+
self, ha_report_issue_func, mock_client
428+
):
429+
"""Test that instructions include new workflow steps."""
430+
mock_client.get_config.return_value = {"version": "2024.12.0"}
431+
mock_client.get_states.return_value = []
432+
433+
result = await ha_report_issue_func()
434+
435+
instructions = result["instructions"]
436+
# Check for new workflow steps
437+
assert "Check for duplicates FIRST" in instructions
438+
assert "duplicate_check_urls" in instructions
439+
assert "suggested_title" in instructions
440+
assert "markdown code block" in instructions.lower()
441+
assert "```markdown" in instructions
442+
assert "PROMINENTLY display the submission URL" in instructions

0 commit comments

Comments
 (0)