Skip to content

Commit de1e4df

Browse files
acailicclaude
andcommitted
feat: improve developer experience — tooltips, validation, prescriptive hints, keyboard shortcuts
Frontend DX: - Add tooltips explaining technical terms (replay_value, drift, policy_shift, ranking factors) - Convert LLMViewer and MultiAgentCoordinationPanel empty states to EmptyState component - Add keyboard shortcuts: Ctrl+1/2/3 for tabs, Ctrl+K or / for search focus - Add skeleton loading animation to CostPanel SDK DX: - Add config validation (endpoint URL, max_payload_kb range, sample_rate range) - Expand boolean parsing to handle 1/0, yes/no, on/off (case-insensitive) - Add actionable HTTP error messages (401/403/404/429) in transport - Add auto-patch console feedback for CLI visibility - Enhance error messages with actionable suggestions for common failure patterns Intelligence: - Add prescriptive action hints in FailureClusterPanel mapped to error types - Add drift direction indicator and remediation guidance in DriftAlertsPanel - Add cost efficiency hint when per-token cost is notably high - Add ranking factor breakdown labels in EventDetail Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bfc7a7b commit de1e4df

13 files changed

Lines changed: 250 additions & 316 deletions

File tree

agent_debugger_sdk/auto_patch/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ def activate(config: PatchConfig | None = None) -> None:
126126
registry.apply(config, names=names)
127127

128128
if registry.patched_names():
129-
logger.info("Peaky Peek auto-patch active for: %s", registry.patched_names())
129+
patched = registry.patched_names()
130+
logger.info("Peaky Peek auto-patch active for: %s", patched)
131+
# Also print to console for CLI users
132+
print(f"agent_debugger: auto-instrumented {', '.join(patched)}")
130133
else:
131134
logger.debug("Peaky Peek auto-patch: no adapters were patched (none available or listed)")
132135

agent_debugger_sdk/config.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66
from dataclasses import dataclass
77

88

9+
def _parse_bool(value: str | None, default: bool = False) -> bool:
10+
"""Parse a boolean value from a string.
11+
12+
Handles: true/false, 1/0, yes/no, on/off (case-insensitive).
13+
Returns default if value is None or cannot be parsed.
14+
"""
15+
if value is None:
16+
return default
17+
normalized = value.strip().lower()
18+
true_values = {"true", "1", "yes", "on"}
19+
false_values = {"false", "0", "no", "off"}
20+
if normalized in true_values:
21+
return True
22+
if normalized in false_values:
23+
return False
24+
return default
25+
26+
927
@dataclass
1028
class Config:
1129
api_key: str | None = None
@@ -15,7 +33,35 @@ class Config:
1533
max_payload_kb: int = 100
1634
mode: str = "local" # "local" or "cloud"
1735

36+
def validate(self) -> None:
37+
"""Validate configuration values.
38+
39+
Raises:
40+
ValueError: If any configuration value is invalid.
41+
"""
42+
if not self.endpoint:
43+
raise ValueError("endpoint_url must be non-empty")
44+
45+
if not self.endpoint.startswith(("http://", "https://")):
46+
raise ValueError(
47+
f"endpoint_url must start with http:// or https://, got: {self.endpoint}"
48+
)
49+
50+
if not isinstance(self.max_payload_kb, int) or self.max_payload_kb <= 0:
51+
raise ValueError(
52+
f"max_payload_kb must be a positive integer, got: {self.max_payload_kb}"
53+
)
54+
55+
if self.max_payload_kb > 10000:
56+
raise ValueError(
57+
f"max_payload_kb must be at most 10000, got: {self.max_payload_kb}"
58+
)
59+
60+
if not isinstance(self.enabled, bool):
61+
raise ValueError(f"enabled must be a boolean, got: {type(self.enabled).__name__}")
62+
1863
def __post_init__(self):
64+
self.validate()
1965
if self.api_key:
2066
self.mode = "cloud"
2167
if self.endpoint == "http://localhost:8000":
@@ -47,13 +93,13 @@ def init(
4793
or ("https://api.agentdebugger.dev" if resolved_key else "http://localhost:8000")
4894
)
4995

50-
resolved_enabled = enabled and os.environ.get("AGENT_DEBUGGER_ENABLED", "true").lower() != "false"
96+
resolved_enabled = enabled and _parse_bool(os.environ.get("AGENT_DEBUGGER_ENABLED"), default=True)
5197

5298
_global_config = Config(
5399
api_key=resolved_key,
54100
endpoint=resolved_endpoint,
55101
enabled=resolved_enabled,
56-
redact_prompts=os.environ.get("AGENT_DEBUGGER_REDACT_PROMPTS", str(redact_prompts)).lower() == "true",
102+
redact_prompts=_parse_bool(os.environ.get("AGENT_DEBUGGER_REDACT_PROMPTS"), default=redact_prompts),
57103
max_payload_kb=int(os.environ.get("AGENT_DEBUGGER_MAX_PAYLOAD_KB", max_payload_kb)),
58104
)
59105
return _global_config

agent_debugger_sdk/core/recorders.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import re
56
from typing import Any
67

78
from .events import (
@@ -24,6 +25,43 @@
2425
)
2526

2627

28+
def _enhance_error_message(error_message: str) -> str:
29+
"""Append helpful suggestions to error messages based on common patterns.
30+
31+
Args:
32+
error_message: The original error message.
33+
34+
Returns:
35+
The enhanced error message with suggestions appended.
36+
"""
37+
suggestions = []
38+
39+
# Check for common error patterns and provide specific suggestions
40+
patterns = [
41+
(r"connection refused", "The server may be down. Check that the agent_debugger server is running."), # noqa: E501
42+
(r"connection.*reset", "The connection was unexpectedly closed. This may indicate a network issue or server restart."), # noqa: E501
43+
(r"timeout", "The request took too long. Consider increasing timeout settings or checking network connectivity."), # noqa: E501
44+
(r"401|unauthorized|authentication", "Check your API key configuration in agent_debugger_sdk.config.init()."), # noqa: E501
45+
(r"403|forbidden|access denied", "Your API key may not have permission for this operation. Verify your credentials."), # noqa: E501
46+
(r"404|not found", "The API endpoint was not found. Check that the server URL is correct."),
47+
(r"429|rate limit", "You are sending requests too quickly. Implement exponential backoff and retry."),
48+
(r"5\d{2}|server error", "The server encountered an error. This is typically a temporary issue. Please retry."),
49+
(r"no route to host", "Network connectivity issue. Check your internet connection and firewall settings."), # noqa: E501
50+
(r"certificate|tls|ssl", "SSL/TLS certificate issue. Ensure the server certificate is valid or check system clock."), # noqa: E501
51+
(r"dns.*not.*resolved|nxdomain", "DNS resolution failed. Verify the server hostname and DNS configuration."), # noqa: E501
52+
]
53+
54+
message_lower = error_message.lower()
55+
for pattern, suggestion in patterns:
56+
if re.search(pattern, message_lower):
57+
suggestions.append(f" Suggestion: {suggestion}")
58+
59+
if suggestions:
60+
# Deduplicate and join suggestions
61+
return error_message + "\n" + "\n".join(set(suggestions))
62+
return error_message
63+
64+
2765
class RecordingMixin:
2866
"""Mixin that records typed trace events through a shared emitter."""
2967

@@ -195,13 +233,16 @@ async def record_error(
195233
) -> str:
196234
self._check_entered()
197235

236+
# Append helpful suggestions for common error patterns
237+
enhanced_message = _enhance_error_message(error_message)
238+
198239
event = ErrorEvent(
199240
session_id=self.session_id,
200241
parent_id=self.get_current_parent(),
201242
event_type=EventType.ERROR,
202243
name=name or f"error_{error_type}",
203244
error_type=error_type,
204-
error_message=error_message,
245+
error_message=enhanced_message,
205246
stack_trace=stack_trace,
206247
importance=0.9,
207248
)

agent_debugger_sdk/transport.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ class PermanentError(TransportError):
3636
DeliveryFailureCallback = Callable[[TransportError], None]
3737

3838

39+
def _get_error_message(status_code: int) -> str:
40+
"""Get a specific, actionable error message for a given HTTP status code."""
41+
messages = {
42+
401: "Authentication failed. Check your API key configuration.",
43+
403: "Access denied. Your API key may not have permission for this operation.",
44+
404: "API endpoint not found. Check that the server URL is correct.",
45+
429: "Rate limited. Please retry after a brief pause.",
46+
}
47+
return messages.get(status_code, f"Client error (status={status_code})")
48+
49+
3950
MAX_RETRIES = 3
4051
INITIAL_BACKOFF_SECONDS = 0.5
4152
BACKOFF_MULTIPLIER = 2.0
@@ -130,8 +141,10 @@ async def _execute_request(
130141
status_code=response.status_code,
131142
)
132143
elif response.status_code >= 400:
144+
# Provide specific, actionable error messages for common status codes
145+
message = _get_error_message(response.status_code)
133146
raise PermanentError(
134-
f"Client error (status={response.status_code})",
147+
message,
135148
status_code=response.status_code,
136149
)
137150

0 commit comments

Comments
 (0)