mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
93cbf6b3f0
* Parse structuredContent from MCP CallToolResult (#3313) The _parse_tool_result_from_mcp method only iterated over the content field from CallToolResult, ignoring the structuredContent field entirely. MCP servers that return JSON data via structuredContent (e.g., Power BI MCP) appeared to return None. Add handling for structuredContent: when present, serialize it as JSON text and append it to the result list. This preserves the data for the LLM while maintaining backward compatibility with existing behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Parse MCP CallToolResult.structuredContent field to prevent tool results returning None Fixes #3313 * Address review feedback: add default=str to json.dumps and remove .checkpoints/ - Add default=str to json.dumps for structuredContent serialization so non-JSON-serializable values (e.g. bytes) degrade gracefully instead of raising TypeError - Remove all .checkpoints/ runtime artifacts from the repository - Add **/.checkpoints/ to .gitignore to prevent future accidental commits - Add test for non-serializable structuredContent values Fixes #3313 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for #3313: Python: MCP CallToolResult.structuredContent field is not parsed, causing tool results to return None --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
128 lines
4.2 KiB
Python
128 lines
4.2 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Scrolling panel for conversation history display."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from textual.widgets import RichLog
|
|
|
|
if TYPE_CHECKING:
|
|
from ..app_state import OutputEntry
|
|
|
|
|
|
class HarnessScrollPanel(RichLog):
|
|
"""Scrolling panel for displaying conversation history.
|
|
|
|
Uses Textual's RichLog widget for efficient append-only rendering with
|
|
Rich text formatting support. Automatically scrolls to the bottom when
|
|
new entries are added.
|
|
|
|
For streaming text, the panel uses a truncate-and-rewrite strategy: it
|
|
tracks where streaming began in the RichLog lines list, and on each update
|
|
truncates back to that point and rewrites the full accumulated text as a
|
|
single write. This ensures consistent rendering without line-break artifacts
|
|
between streamed chunks.
|
|
"""
|
|
|
|
def __init__(self, **kwargs) -> None:
|
|
"""Initialize the scroll panel.
|
|
|
|
Args:
|
|
**kwargs: Additional arguments passed to RichLog.
|
|
"""
|
|
super().__init__(
|
|
**kwargs,
|
|
auto_scroll=True, # Automatically scroll to bottom
|
|
wrap=True, # Wrap long lines instead of horizontal scroll
|
|
markup=True, # Enable Rich markup
|
|
highlight=True, # Enable syntax highlighting
|
|
)
|
|
self._entries: list[OutputEntry] = []
|
|
self._is_streaming = False
|
|
self._streaming_line_start: int = 0
|
|
|
|
def append_entry(self, entry: OutputEntry) -> None:
|
|
"""Append a new output entry to the conversation history.
|
|
|
|
Args:
|
|
entry: The output entry to append.
|
|
"""
|
|
self._entries.append(entry)
|
|
text = self._format_entry(entry)
|
|
self.write(text)
|
|
|
|
def set_streaming_entry(self, entry: OutputEntry) -> None:
|
|
"""Set or update the current streaming entry.
|
|
|
|
On each update, truncates the RichLog back to where streaming
|
|
started, then rewrites the full streaming text as a single block.
|
|
This ensures no spurious line breaks between chunks while avoiding
|
|
a full rewrite of all entries.
|
|
|
|
Args:
|
|
entry: The streaming entry (will be mutated externally).
|
|
"""
|
|
if not self._is_streaming:
|
|
# First streaming chunk — record where streaming lines begin
|
|
self._is_streaming = True
|
|
self._entries.append(entry)
|
|
self._streaming_line_start = len(self.lines)
|
|
|
|
# Truncate lines back to where streaming started
|
|
if len(self.lines) > self._streaming_line_start:
|
|
del self.lines[self._streaming_line_start :]
|
|
from textual.geometry import Size
|
|
|
|
self.virtual_size = Size(self._widest_line_width, len(self.lines))
|
|
|
|
# Write full streaming text as a single renderable
|
|
formatted = self._format_text(entry.text, entry.color)
|
|
self.write(formatted)
|
|
|
|
def end_streaming(self) -> None:
|
|
"""End the current streaming mode."""
|
|
if self._is_streaming:
|
|
self._is_streaming = False
|
|
self._streaming_line_start = 0
|
|
|
|
def _rewrite_all(self) -> None:
|
|
"""Clear and rewrite all entries from scratch."""
|
|
self.clear()
|
|
for entry in self._entries:
|
|
self.write(self._format_entry(entry))
|
|
|
|
def _format_entry(self, entry: OutputEntry) -> str:
|
|
"""Format an output entry with Rich markup.
|
|
|
|
Args:
|
|
entry: The entry to format.
|
|
|
|
Returns:
|
|
Formatted string with Rich markup for color and styling.
|
|
"""
|
|
return self._format_text(entry.text, entry.color)
|
|
|
|
@staticmethod
|
|
def _format_text(text: str, color: str | None) -> str:
|
|
"""Format text with optional Rich color markup.
|
|
|
|
Args:
|
|
text: The text to format.
|
|
color: Optional Rich color name.
|
|
|
|
Returns:
|
|
Formatted string.
|
|
"""
|
|
if color:
|
|
return f"[{color}]{text}[/{color}]"
|
|
return text
|
|
|
|
def clear_history(self) -> None:
|
|
"""Clear all conversation history from the panel."""
|
|
self._entries.clear()
|
|
self._is_streaming = False
|
|
self._streaming_line_start = 0
|
|
self.clear()
|