From 6fc1f309fe8bb56318366943d5806ab3cda91f6a Mon Sep 17 00:00:00 2001 From: Jalen Yan Date: Sat, 7 Mar 2026 14:54:43 +0800 Subject: [PATCH 1/2] fix: resolve empty response display and streaming errors - Add helper methods to StreamUpdate dataclass for progress display - Fix ConversationEnhancer.update_context() call signature - Generate tool summary when response content is empty Fixes RichardAtCT/claude-code-telegram#135 --- src/bot/handlers/message.py | 16 ++--- src/claude/sdk_integration.py | 120 ++++++++++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 15 deletions(-) diff --git a/src/bot/handlers/message.py b/src/bot/handlers/message.py index e5fa9f78..1641a19a 100644 --- a/src/bot/handlers/message.py +++ b/src/bot/handlers/message.py @@ -590,22 +590,16 @@ async def stream_handler(update_obj): if conversation_enhancer and claude_response: try: # Update conversation context - conversation_context = conversation_enhancer.update_context( - session_id=claude_response.session_id, - user_id=user_id, - working_directory=str(current_dir), - tools_used=claude_response.tools_used or [], - response_content=claude_response.content, + conversation_enhancer.update_context(user_id, claude_response) + conversation_context = conversation_enhancer.get_or_create_context( + user_id ) # Check if we should show follow-up suggestions - if conversation_enhancer.should_show_suggestions( - claude_response.tools_used or [], claude_response.content - ): + if conversation_enhancer.should_show_suggestions(claude_response): # Generate follow-up suggestions suggestions = conversation_enhancer.generate_follow_up_suggestions( - claude_response.content, - claude_response.tools_used or [], + claude_response, conversation_context, ) diff --git a/src/claude/sdk_integration.py b/src/claude/sdk_integration.py index adf553f4..48ad64ed 100644 --- a/src/claude/sdk_integration.py +++ b/src/claude/sdk_integration.py @@ -61,8 +61,110 @@ class StreamUpdate: type: str # 'assistant', 'user', 'system', 'result', 'stream_delta' content: Optional[str] = None - tool_calls: Optional[List[Dict]] = None - metadata: Optional[Dict] = None + tool_calls: Optional[List[Dict[str, Any]]] = None + metadata: Optional[Dict[str, Any]] = None + progress: Optional[Dict[str, Any]] = None + + def get_tool_names(self) -> List[str]: + """Return tool names from the stream payload.""" + names: List[str] = [] + + if self.tool_calls: + for tool_call in self.tool_calls: + name = tool_call.get("name") if isinstance(tool_call, dict) else None + if isinstance(name, str) and name: + names.append(name) + + if self.metadata: + tool_name = self.metadata.get("tool_name") + if isinstance(tool_name, str) and tool_name: + names.append(tool_name) + + metadata_tools = self.metadata.get("tools") + if isinstance(metadata_tools, list): + for tool in metadata_tools: + if isinstance(tool, dict): + name = tool.get("name") + elif isinstance(tool, str): + name = tool + else: + name = None + + if isinstance(name, str) and name: + names.append(name) + + # Preserve insertion order while de-duplicating. + return list(dict.fromkeys(names)) + + def is_error(self) -> bool: + """Check whether this stream update represents an error.""" + if self.type == "error": + return True + + if self.metadata: + if self.metadata.get("is_error") is True: + return True + status = self.metadata.get("status") + if isinstance(status, str) and status.lower() == "error": + return True + if self.metadata.get("error") or self.metadata.get("error_message"): + return True + + if self.progress: + status = self.progress.get("status") + if isinstance(status, str) and status.lower() == "error": + return True + + return False + + def get_error_message(self) -> str: + """Get the best available error message from the stream payload.""" + if self.metadata: + for key in ("error_message", "error", "message"): + value = self.metadata.get(key) + if isinstance(value, str) and value.strip(): + return value + + if isinstance(self.content, str) and self.content.strip(): + return self.content + + if self.progress: + value = self.progress.get("error") + if isinstance(value, str) and value.strip(): + return value + + return "Unknown error" + + def get_progress_percentage(self) -> Optional[int]: + """Extract progress percentage if present.""" + + def _to_int(value: Any) -> Optional[int]: + if isinstance(value, (int, float)): + return int(value) + if isinstance(value, str) and value.strip(): + try: + return int(float(value)) + except ValueError: + return None + return None + + if self.progress: + for key in ("percentage", "percent", "progress"): + percentage = _to_int(self.progress.get(key)) + if percentage is not None: + return max(0, min(100, percentage)) + + step = _to_int(self.progress.get("step")) + total_steps = _to_int(self.progress.get("total_steps")) + if step is not None and total_steps and total_steps > 0: + return max(0, min(100, int((step / total_steps) * 100))) + + if self.metadata: + percentage = _to_int(self.metadata.get("progress_percentage")) + if percentage is not None: + return max(0, min(100, percentage)) + + return None def _make_can_use_tool_callback( @@ -350,7 +452,7 @@ async def _run_client() -> None: # Use ResultMessage.result if available, fall back to message extraction if result_content is not None: - content = result_content + content = str(result_content).strip() else: content_parts = [] for msg in messages: @@ -362,7 +464,17 @@ async def _run_client() -> None: content_parts.append(block.text) elif msg_content: content_parts.append(str(msg_content)) - content = "\n".join(content_parts) + content = "\n".join(content_parts).strip() + + if not content and tools_used: + tool_names = [ + tool.get("name", "") + for tool in tools_used + if isinstance(tool.get("name"), str) and tool.get("name") + ] + unique_tool_names = list(dict.fromkeys(tool_names)) + tools_summary = ", ".join(unique_tool_names) or "unknown" + content = f"✅ Task completed. Tools used: {tools_summary}" return ClaudeResponse( content=content, From de1550b236eec8d57bffcfa3d37a6618a056e8cf Mon Sep 17 00:00:00 2001 From: Jalen Yan Date: Sat, 7 Mar 2026 15:58:27 +0800 Subject: [PATCH 2/2] fix: address PR #136 review feedback - is_error(): use isinstance() type guards to prevent false positives from non-string truthy values like {} or 0 - Extract hardcoded task-completed string as TASK_COMPLETED_MSG constant for future i18n - Verified get_or_create_context/generate_follow_up_suggestions type compatibility (ConversationContext matches, no change needed) --- src/claude/sdk_integration.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/claude/sdk_integration.py b/src/claude/sdk_integration.py index 48ad64ed..a92ce40b 100644 --- a/src/claude/sdk_integration.py +++ b/src/claude/sdk_integration.py @@ -40,6 +40,9 @@ logger = structlog.get_logger() +# Fallback message when Claude produces no text but did use tools. +TASK_COMPLETED_MSG = "✅ Task completed. Tools used: {tools_summary}" + @dataclass class ClaudeResponse: @@ -107,7 +110,11 @@ def is_error(self) -> bool: status = self.metadata.get("status") if isinstance(status, str) and status.lower() == "error": return True - if self.metadata.get("error") or self.metadata.get("error_message"): + error_val = self.metadata.get("error") + if isinstance(error_val, str) and error_val: + return True + error_msg_val = self.metadata.get("error_message") + if isinstance(error_msg_val, str) and error_msg_val: return True if self.progress: @@ -474,7 +481,7 @@ async def _run_client() -> None: ] unique_tool_names = list(dict.fromkeys(tool_names)) tools_summary = ", ".join(unique_tool_names) or "unknown" - content = f"✅ Task completed. Tools used: {tools_summary}" + content = TASK_COMPLETED_MSG.format(tools_summary=tools_summary) return ClaudeResponse( content=content,