Files
Enrico Toniato 266a445934 Canary (#524)
* chore: enter canary prerelease mode

* Improve search tool response structure (#508)

* Refactor search_tools to return metadata and results

Co-authored-by: tonxipad <tonxipad@gmail.com>

* docs: update API documentation

* feat: enhance search_tools display and filtering capabilities

- Introduced a new function to format search_tools results as a tree structure, including metadata such as total tools and namespaces.
- Updated the display logic to handle both new and old formats of search_tools results for backward compatibility.
- Enhanced filtering in MCPClient and CodeExecutor to include server name in search criteria, improving tool discoverability.

* format

* docs: update code-mode documentation to reflect changes in search_tools response structure

- Modified examples to use the new response format from search_tools, which includes metadata and results.
- Enhanced clarity in code snippets by adding print statements for tool discovery results.
- Updated TypeScript and Python documentation to ensure consistency across both languages.

* refactor: clean up whitespace in display.py for improved readability

- Removed unnecessary blank lines throughout the _format_search_tools_as_tree function to enhance code clarity and maintainability.
- Ensured consistent formatting in the display logic for better visual structure.

* refactor: streamline line wrapping in display.py for improved readability

- Consolidated parameters in the text wrapping function to enhance code clarity and maintainability.

* feat: enhance search_tools metadata for improved context

- Updated search_tools to return additional metadata (total_tools, namespaces, result_count) alongside results, enhancing the context for model decision-making.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: tonxipad <tonxipad@gmail.com>

* chore(typescript): version packages (canary)

* feat: Integrate official UI components in templates and enhance widget development experience (#517)

* feat: Add ecommerce widgets and OpenAI Apps SDK UI components

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Update MCP app templates and resources

- Added new fruit images to the public directory for enhanced visual content.
- Removed outdated weather and ecommerce carousel components to streamline the codebase.
- Updated the product search result component to utilize new UI components and improved error handling.
- Modified package dependencies to include react-router and updated @openai/apps-sdk-ui version.
- Enhanced the .gitignore file to include .mcp-use for better project management.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Introduce new lockfile and update dependencies

- Added a new bun.lock file to manage package dependencies for the TypeScript project.
- Updated pnpm-lock.yaml to include new dependencies such as @openai/apps-sdk-ui and react-router.
- Enhanced the CLI tool to support aborting fetch requests and improved error handling in widget building.
- Replaced outdated image assets with PNG versions for better compatibility and removed unused JPG files.
- Introduced new ErrorBoundary and Image components to improve error handling and image loading in the MCP app.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Enhance Vite server configuration and update dependencies

- Added a new plugin to Vite for watching the resources directory, ensuring hot module replacement (HMR) works seamlessly when widget source files change.
- Updated pnpm-lock.yaml to include new dependencies such as @openai/apps-sdk-ui, react-router, and various development tools like @tailwindcss/vite and typescript.
- Improved the Vite server configuration to explicitly watch files outside the root directory, enhancing the development experience.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Implement carousel component for product search results

- Added a new CarouselItem component to enhance the visual presentation of fruit items in the product search results.
- Introduced dynamic pointer tracking for interactive hover effects on carousel items, improving user engagement.
- Updated styles for carousel items and their backgrounds to create a visually appealing blur effect.
- Refactored the ProductSearchResult component to utilize the new carousel structure and improve layout responsiveness.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Update ProductSearchResult and OpenAIComponentRenderer for theme support

- Added WidgetDebugger to the ProductSearchResult component for enhanced debugging capabilities.
- Updated the product search result text to reflect a new template and improved context for users.
- Enhanced OpenAIComponentRenderer to handle theme changes, ensuring the correct theme is applied when the iframe loads.
- Modified shared-utils to include theme information in WidgetData, allowing for better theme management across components.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Refactor ProductSearchResult component and enhance WidgetFullscreenWrapper

- Replaced ThemeProvider and BrowserRouter with McpUseProvider in the ProductSearchResult component for improved context management.
- Removed unused getBasename function to streamline routing logic.
- Updated WidgetFullscreenWrapper to support viewControls prop, allowing conditional rendering of fullscreen and picture-in-picture buttons.
- Enhanced overall structure and readability of the ProductSearchResult component.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Update dependencies and enhance component functionality

- Added `react-router-dom` as a dependency to support routing features.
- Updated `detect-libc` to version 2.1.2 for improved compatibility.
- Refactored various components for better readability and structure, including the `ProductSearchResult` and `OpenAIComponentRenderer`.
- Introduced new auto-sizing functionality in `McpUseProvider` to notify OpenAI about container height changes.
- Removed deprecated `WidgetDebugger` and `WidgetFullscreenWrapper` components to streamline the codebase.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* refactor: Enhance OpenAIComponentRenderer and ToolResultDisplay for performance and readability

- Memoized component to prevent unnecessary re-renders in OpenAIComponentRenderer.
- Introduced new state management for iframe height notifications and improved height handling logic.
- Refactored ToolResultDisplay to utilize memoization for args and result, optimizing rendering performance.
- Cleaned up unused state variables and streamlined event handling for better maintainability.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* refactor: Improve logging and code readability in various components

- Enhanced logging statements in OpenAIComponentRenderer, McpUseProvider, and WidgetControls for better debugging and clarity.
- Refactored memoization in ToolResultDisplay to improve performance and prevent unnecessary re-renders.
- Cleaned up code formatting for improved readability across multiple components.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* refactor: Update ProductSearchResult and shared routes for improved development experience

- Modified the ProductSearchResult component to change the FAQ question and enhance the autosize feature description.
- Simplified asset loading in shared routes to directly fetch from the dev server, improving development efficiency.
- Enhanced security headers in shared-utils to include dev server origins for better handling of HMR scripts and resource loading.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Integrate @tanstack/react-query and enhance fruits API in apps-sdk template

- Added @tanstack/react-query as a dependency to improve data fetching capabilities.
- Implemented a new API endpoint in the apps-sdk template to serve a list of fruits with their respective colors.
- Removed the outdated ProductSearchResult component to streamline the codebase and focus on the new API integration.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* refactor: Clean up code formatting and improve readability in product search result components

- Removed unnecessary blank lines in various TypeScript files to enhance code clarity.
- Simplified function parameters in CarouselItem component for better readability.
- Streamlined the Accordion and AccordionItem components by removing excess whitespace.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* change set

* chore: Remove bun.lock file to streamline dependency management

- Deleted the bun.lock file to eliminate potential conflicts and simplify the dependency resolution process in the project.

* feat: Update documentation and components for improved clarity and organization

- Added new pages for MCP UI resources and widget components in the documentation.
- Enhanced the migration guide to specify applicability for version 1.5.0 and later.
- Updated the ErrorBoundary component icon for better representation.
- Removed outdated template comparison section to streamline the templates documentation.

* fix: Correct indentation in documentation JSON for better readability

- Adjusted the indentation of the "apps-sdk-resources" entry in the documentation JSON to maintain consistent formatting.
- Removed unnecessary blank lines to enhance overall clarity in the documentation structure.

* chore: Update dependencies and clean up code for improved performance

- Updated react-router-dom to version 7.9.5 for enhanced functionality.
- Removed commented-out code in ProductSearchResult component to improve clarity.
- Cleaned up logging statements in OpenAIComponentRenderer for better debugging.
- Refactored height notification logic in McpUseProvider to optimize performance.
- Adjusted useWidget to avoid dependency issues with URL parameters.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* chore: Update dependencies and clean up code for improved performance

- Updated react-router-dom to version 7.9.5 for enhanced functionality.
- Removed commented-out code in ProductSearchResult component to improve clarity.
- Cleaned up logging statements in OpenAIComponentRenderer for better debugging.
- Refactored height notification logic in McpUseProvider to optimize performance.
- Adjusted useWidget to avoid dependency issues with URL parameters.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* refactor: Clean up code formatting and improve readability in OpenAIComponentRenderer and useWidget

- Adjusted formatting of variable declarations for better consistency and readability.
- Simplified the handleError function in OpenAIComponentRenderer for clarity.
- Ensured consistent indentation in useWidget for improved code structure.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: tonxipad <tonxipad@gmail.com>

* chore(typescript): version packages (canary)

* feat: Add JSON-RPC message logging to Inspector tabs (MCP-536) (#519)

* feat: Add ecommerce widgets and OpenAI Apps SDK UI components

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Update MCP app templates and resources

- Added new fruit images to the public directory for enhanced visual content.
- Removed outdated weather and ecommerce carousel components to streamline the codebase.
- Updated the product search result component to utilize new UI components and improved error handling.
- Modified package dependencies to include react-router and updated @openai/apps-sdk-ui version.
- Enhanced the .gitignore file to include .mcp-use for better project management.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Introduce new lockfile and update dependencies

- Added a new bun.lock file to manage package dependencies for the TypeScript project.
- Updated pnpm-lock.yaml to include new dependencies such as @openai/apps-sdk-ui and react-router.
- Enhanced the CLI tool to support aborting fetch requests and improved error handling in widget building.
- Replaced outdated image assets with PNG versions for better compatibility and removed unused JPG files.
- Introduced new ErrorBoundary and Image components to improve error handling and image loading in the MCP app.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Enhance Vite server configuration and update dependencies

- Added a new plugin to Vite for watching the resources directory, ensuring hot module replacement (HMR) works seamlessly when widget source files change.
- Updated pnpm-lock.yaml to include new dependencies such as @openai/apps-sdk-ui, react-router, and various development tools like @tailwindcss/vite and typescript.
- Improved the Vite server configuration to explicitly watch files outside the root directory, enhancing the development experience.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Implement carousel component for product search results

- Added a new CarouselItem component to enhance the visual presentation of fruit items in the product search results.
- Introduced dynamic pointer tracking for interactive hover effects on carousel items, improving user engagement.
- Updated styles for carousel items and their backgrounds to create a visually appealing blur effect.
- Refactored the ProductSearchResult component to utilize the new carousel structure and improve layout responsiveness.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Update ProductSearchResult and OpenAIComponentRenderer for theme support

- Added WidgetDebugger to the ProductSearchResult component for enhanced debugging capabilities.
- Updated the product search result text to reflect a new template and improved context for users.
- Enhanced OpenAIComponentRenderer to handle theme changes, ensuring the correct theme is applied when the iframe loads.
- Modified shared-utils to include theme information in WidgetData, allowing for better theme management across components.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Refactor ProductSearchResult component and enhance WidgetFullscreenWrapper

- Replaced ThemeProvider and BrowserRouter with McpUseProvider in the ProductSearchResult component for improved context management.
- Removed unused getBasename function to streamline routing logic.
- Updated WidgetFullscreenWrapper to support viewControls prop, allowing conditional rendering of fullscreen and picture-in-picture buttons.
- Enhanced overall structure and readability of the ProductSearchResult component.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Update dependencies and enhance component functionality

- Added `react-router-dom` as a dependency to support routing features.
- Updated `detect-libc` to version 2.1.2 for improved compatibility.
- Refactored various components for better readability and structure, including the `ProductSearchResult` and `OpenAIComponentRenderer`.
- Introduced new auto-sizing functionality in `McpUseProvider` to notify OpenAI about container height changes.
- Removed deprecated `WidgetDebugger` and `WidgetFullscreenWrapper` components to streamline the codebase.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* refactor: Enhance OpenAIComponentRenderer and ToolResultDisplay for performance and readability

- Memoized component to prevent unnecessary re-renders in OpenAIComponentRenderer.
- Introduced new state management for iframe height notifications and improved height handling logic.
- Refactored ToolResultDisplay to utilize memoization for args and result, optimizing rendering performance.
- Cleaned up unused state variables and streamlined event handling for better maintainability.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* refactor: Improve logging and code readability in various components

- Enhanced logging statements in OpenAIComponentRenderer, McpUseProvider, and WidgetControls for better debugging and clarity.
- Refactored memoization in ToolResultDisplay to improve performance and prevent unnecessary re-renders.
- Cleaned up code formatting for improved readability across multiple components.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* refactor: Update ProductSearchResult and shared routes for improved development experience

- Modified the ProductSearchResult component to change the FAQ question and enhance the autosize feature description.
- Simplified asset loading in shared routes to directly fetch from the dev server, improving development efficiency.
- Enhanced security headers in shared-utils to include dev server origins for better handling of HMR scripts and resource loading.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Integrate @tanstack/react-query and enhance fruits API in apps-sdk template

- Added @tanstack/react-query as a dependency to improve data fetching capabilities.
- Implemented a new API endpoint in the apps-sdk template to serve a list of fruits with their respective colors.
- Removed the outdated ProductSearchResult component to streamline the codebase and focus on the new API integration.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* refactor: Clean up code formatting and improve readability in product search result components

- Removed unnecessary blank lines in various TypeScript files to enhance code clarity.
- Simplified function parameters in CarouselItem component for better readability.
- Streamlined the Accordion and AccordionItem components by removing excess whitespace.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* feat: Integrate JsonRpcLoggerView into PromptsTab, ResourcesTab, and ToolsTab for enhanced logging

- Added JsonRpcLoggerView component to PromptsTab, ResourcesTab, and ToolsTab to facilitate RPC logging.
- Adjusted ResizablePanel default sizes for better layout consistency across tabs.
- Implemented keyboard shortcut handling in ToolExecutionPanel for executing tools with Cmd/Ctrl + Enter.

Co-authored-by: tonxipad <tonxipad@gmail.com>

* format

* chore: changeset

* docs: add rpc docs

* chore: Remove bun.lock file and clean up pnpm-lock.yaml

- Deleted the bun.lock file to streamline dependency management and eliminate potential conflicts.
- Removed outdated dependencies from pnpm-lock.yaml, including entries for test_app and its associated packages, to simplify the lockfile and improve clarity.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: tonxipad <tonxipad@gmail.com>

* chore(typescript): version packages (canary)

* docs: update API documentation

* docs: api update docs

* trigger release

* chore: Clean up pnpm-lock.yaml and remove bun.lock file for streamlined dependency management

* chore(typescript): version packages (canary)

* chore(inspector): update package.json with homepage, repository, and bugs information

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: tonxipad <tonxipad@gmail.com>
2025-11-24 20:59:22 +01:00

563 lines
23 KiB
Python

import json
import logging
import re
import textwrap
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.pretty import Pretty
from rich.text import Text
console = Console()
logger = logging.getLogger(__name__)
# Set code theme as a global variable
CODE_THEME = "stata-dark"
def _extract_json_from_textcontent(result):
"""Extract JSON from TextContent objects in result string."""
result_str = str(result)
# Try to extract JSON between text=' and ', annotations=
# Use greedy matching since we know the specific end pattern
pattern = r"text='(.*)',\s*annotations="
match = re.search(pattern, result_str, re.DOTALL)
if match:
json_str = match.group(1)
# Unescape the string - Python's str() escapes quotes and backslashes
json_str = json_str.replace("\\'", "'")
json_str = json_str.replace("\\\\", "\\")
try:
return json.loads(json_str)
except json.JSONDecodeError:
pass
return None
def _format_search_tools_as_tree(tools, meta=None, query=None):
"""Format search_tools result as a tree structure.
Args:
tools: List of tool dictionaries with 'server', 'name', and optionally 'description'
meta: Optional dictionary with 'total_tools', 'namespaces', 'result_count'
query: Optional query string that was used for the search
Returns:
Formatted tree string
"""
# Build meta information display
meta_lines = []
if meta:
if meta.get("total_tools") is not None:
meta_lines.append(f"Total tools: {meta['total_tools']}")
if meta.get("namespaces") and len(meta["namespaces"]) > 0:
meta_lines.append(f"Namespaces: {', '.join(meta['namespaces'])}")
if meta.get("result_count") is not None:
meta_lines.append(f"Results: {meta['result_count']}")
if not isinstance(tools, list) or len(tools) == 0:
no_results_msg = f'No tools found for query "{query}"' if query else "(no tools found)"
if meta_lines:
return "\n".join(meta_lines) + "\n\n" + no_results_msg
return no_results_msg
# Group tools by server
tools_by_server = {}
for tool in tools:
server = tool.get("server", "unknown")
if server not in tools_by_server:
tools_by_server[server] = []
tools_by_server[server].append(tool)
# Build tree structure
lines = []
# Add meta information at the top if available
if meta_lines:
lines.extend(meta_lines)
lines.append("") # Empty line before tree
servers = sorted(tools_by_server.keys())
for i, server in enumerate(servers):
server_tools = tools_by_server[server]
is_last_server = i == len(servers) - 1
server_prefix = "└─" if is_last_server else "├─"
# Use rich Text for colored output
server_text = Text(f"{server_prefix} ")
server_text.append(server, style="cyan")
server_text.append(f" ({len(server_tools)} tools)")
lines.append(server_text)
# Add tools under this server
for j, tool in enumerate(server_tools):
is_last_tool = j == len(server_tools) - 1
indent = " " if is_last_server else "│ "
tool_prefix = "└─" if is_last_tool else "├─"
# Tool name line
tool_name = tool.get("name", "unknown")
tool_text = Text(f"{indent}{tool_prefix} {tool_name}")
lines.append(tool_text)
# Description on new line, aligned with tool name
if tool.get("description"):
# Calculate indent for description lines
# Use the same base indent as the tool, then add alignment
# If not the last tool, add vertical bar to show continuation, otherwise spaces
desc_align = " " if is_last_tool else "│ "
description_indent = indent + desc_align
description = tool["description"]
# Get terminal width for wrapping (default to 120 if not available)
try:
terminal_width = console.width
except Exception:
terminal_width = 120
# Calculate available width for description
# Account for: indent + Panel padding (approximately 4 chars for borders/padding)
indent_length = len(description_indent)
available_width = max(40, terminal_width - indent_length - 4)
# Wrap description text at word boundaries
wrapped_lines = textwrap.wrap(
description, width=available_width, break_long_words=False, break_on_hyphens=False
)
# Add indent to each line (including continuation lines)
for desc_line in wrapped_lines:
desc_text = Text(description_indent)
desc_text.append(desc_line, style="dim")
lines.append(desc_text)
# Convert to string for display
result_lines = []
for line in lines:
if isinstance(line, Text):
result_lines.append(line.plain)
else:
result_lines.append(str(line))
return "\n".join(result_lines)
def _render_content(content):
"""Render content with appropriate syntax highlighting using markdown code blocks."""
if content is None:
return Markdown("```\nNone\n```", code_theme=CODE_THEME)
content_str = str(content) if not isinstance(content, str) else content
content_str_stripped = content_str.strip()
# Try to detect and render JSON
if content_str_stripped.startswith(("{", "[")):
try:
parsed = json.loads(content_str_stripped)
formatted = json.dumps(parsed, indent=2)
return Markdown(f"```json\n{formatted}\n```", code_theme=CODE_THEME)
except json.JSONDecodeError:
pass
# For dicts and lists, convert to JSON
if isinstance(content, dict | list):
try:
formatted = json.dumps(content, indent=2)
return Markdown(f"```json\n{formatted}\n```", code_theme=CODE_THEME)
except (TypeError, ValueError):
pass
# If it already has markdown code blocks, render as is
if "```" in content_str:
return Markdown(content_str, code_theme=CODE_THEME)
# Detect markdown patterns (headers, lists)
if re.search(r"^#{1,6}\s|^\*\s|^-\s", content_str_stripped, re.MULTILINE):
return Markdown(content_str, code_theme=CODE_THEME)
# Detect code patterns
if "\n" in content_str and any(kw in content_str for kw in ["def ", "class ", "import ", "function ", "const "]):
# Try to detect language
if "def " in content_str or "import " in content_str:
return Markdown(f"```python\n{content_str}\n```", code_theme=CODE_THEME)
elif "function " in content_str or "const " in content_str:
return Markdown(f"```javascript\n{content_str}\n```", code_theme=CODE_THEME)
# Default: plain text in markdown code block
return Markdown(f"```\n{content_str}\n```", code_theme=CODE_THEME)
def _pretty_print_step(item):
"""Pretty print agent step using rich formatting."""
if isinstance(item, tuple) and len(item) == 2:
action, result = item
tool_name = getattr(action, "tool", None)
tool_input = getattr(action, "tool_input", {})
console.print() # Empty line before tool
if tool_input:
if tool_name == "execute_code" and "code" in tool_input:
code = tool_input["code"]
code_md = Markdown(f"```python\n{code}\n```", code_theme=CODE_THEME)
title = f"[dim]🔧[/dim] [bold white]{tool_name}[/bold white] [dim]Input[/dim]"
console.print(Panel(code_md, title=title, border_style="dim white", padding=(0, 1)))
other_inputs = {k: v for k, v in tool_input.items() if k != "code"}
if other_inputs:
console.print(
Panel(
_render_content(other_inputs),
title="[dim]Other Parameters[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
else:
title = f"[dim]🔧[/dim] [bold white]{tool_name}[/bold white] [dim]Input[/dim]"
console.print(
Panel(
_render_content(tool_input),
title=title,
border_style="dim white",
padding=(0, 1),
)
)
if result:
parsed_json = _extract_json_from_textcontent(result)
if parsed_json:
# Special handling for search_tools to display as tree
if tool_name == "search_tools":
# Get query from tool input
query = tool_input.get("query") if isinstance(tool_input, dict) else None
# Handle new format: object with meta and results
if isinstance(parsed_json, dict) and "results" in parsed_json:
results = parsed_json["results"]
meta = parsed_json.get("meta")
if isinstance(results, list):
tree_str = _format_search_tools_as_tree(results, meta, query)
console.print(
Panel(
Markdown(f"```\n{tree_str}\n```", code_theme=CODE_THEME),
title="[dim]Result[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
return
# Handle old format: direct array (backward compatibility)
elif isinstance(parsed_json, list):
query = tool_input.get("query") if isinstance(tool_input, dict) else None
tree_str = _format_search_tools_as_tree(parsed_json, None, query)
console.print(
Panel(
Markdown(f"```\n{tree_str}\n```", code_theme=CODE_THEME),
title="[dim]Result[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
return
if tool_name == "execute_code" and isinstance(parsed_json, dict):
execution_time = parsed_json.get("execution_time")
time_str = f" [dim]⏱️ {execution_time:.3f}s[/dim]" if execution_time is not None else ""
has_logs = "logs" in parsed_json and parsed_json["logs"]
has_result = "result" in parsed_json and parsed_json["result"] is not None
if has_result:
# Add execution time to Result only if there are no logs (logs will get the time)
result_title = f"[dim]Result[/dim]{time_str}" if not has_logs else "[dim]Result[/dim]"
console.print(
Panel(
_render_content(parsed_json["result"]),
title=result_title,
border_style="dim white",
padding=(0, 1),
)
)
if has_logs:
# Add execution time to Logs panel (last panel)
console.print(
Panel(
_render_content(parsed_json["logs"]),
title=f"[dim]Logs[/dim]{time_str}",
border_style="dim white",
padding=(0, 1),
)
)
if "error" in parsed_json and parsed_json["error"] is not None:
console.print(
Panel(
_render_content(parsed_json["error"]),
title="[dim red]Error[/dim red]",
border_style="red",
padding=(0, 1),
)
)
else:
console.print(
Panel(
_render_content(parsed_json),
title="[dim]Result[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
else:
console.print(
Panel(
_render_content(result),
title="[dim]Result[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
else:
console.print(Pretty(item))
def _log_step(item):
"""Log agent step using logger.info with emoji messages."""
if isinstance(item, tuple) and len(item) == 2:
action, result = item
tool_name = getattr(action, "tool", None)
tool_input = getattr(action, "tool_input", {})
tool_input_str = str(tool_input)
if len(tool_input_str) > 100:
tool_input_str = tool_input_str[:97] + "..."
logger.info(f"🔧 Tool call: {tool_name} with input: {tool_input_str}")
observation_str = str(result)
if len(observation_str) > 100:
observation_str = observation_str[:97] + "..."
observation_str = observation_str.replace("\n", " ")
logger.info(f"📄 Tool result: {observation_str}")
else:
logger.info(f"Agent step: {item}")
def log_agent_step(item, pretty_print=True):
"""Display or log an agent step based on pretty_print flag."""
if pretty_print:
_pretty_print_step(item)
else:
_log_step(item)
def log_agent_stream(chunk, pretty_print=True):
"""Handle streaming events from astream_events.
Args:
chunk: Event dictionary from LangChain's astream_events
pretty_print: If True, use rich formatting. If False, do nothing.
"""
if not pretty_print:
return
event_type = chunk.get("event")
event_data = chunk.get("data", {})
name = chunk.get("name", "")
# Only process specific event types we care about
if event_type not in ("on_tool_start", "on_tool_end", "on_chat_model_stream"):
return
# Handle tool start events
if event_type == "on_tool_start":
tool_input = event_data.get("input", {})
tool_name = name.split("/")[-1] if "/" in name else name
console.print() # Empty line before tool
if tool_input:
if tool_name == "execute_code" and "code" in tool_input:
code = tool_input["code"]
code_md = Markdown(f"```python\n{code}\n```", code_theme=CODE_THEME)
title = f"[dim]🔧[/dim] [bold white]{tool_name}[/bold white] [dim]Input[/dim]"
console.print(Panel(code_md, title=title, border_style="dim white", padding=(0, 1)))
other_inputs = {k: v for k, v in tool_input.items() if k != "code"}
if other_inputs:
console.print(
Panel(
_render_content(other_inputs),
title="[dim]Other Parameters[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
else:
title = f"[dim]🔧[/dim] [bold white]{tool_name}[/bold white] [dim]Input[/dim]"
console.print(
Panel(
_render_content(tool_input),
title=title,
border_style="dim white",
padding=(0, 1),
)
)
# Handle tool end events
elif event_type == "on_tool_end":
output = event_data.get("output", "")
if output:
# Extract content from ToolMessage if it's a ToolMessage object
output_str = None
if hasattr(output, "content"):
content = output.content
# Handle list of TextContent objects
if isinstance(content, list) and content:
# Try to extract text from TextContent objects
text_contents = []
for item in content:
if hasattr(item, "text"):
text_contents.append(item.text)
elif isinstance(item, dict) and "text" in item:
text_contents.append(item["text"])
if text_contents:
output_str = text_contents[0] if len(text_contents) == 1 else str(text_contents)
else:
output_str = str(content)
elif isinstance(content, str):
output_str = content
else:
output_str = str(content)
elif isinstance(output, dict) and "content" in output:
output_str = str(output["content"])
else:
output_str = str(output)
parsed_json = _extract_json_from_textcontent(output_str) if output_str else None
if parsed_json:
tool_name = name.split("/")[-1] if "/" in name else name
# Special handling for search_tools to display as tree
if tool_name == "search_tools":
# Try to get query from event data input
event_input = event_data.get("input", {}) if event_data else {}
query = event_input.get("query") if isinstance(event_input, dict) else None
# Handle new format: object with meta and results
if isinstance(parsed_json, dict) and "results" in parsed_json:
results = parsed_json["results"]
meta = parsed_json.get("meta")
if isinstance(results, list):
tree_str = _format_search_tools_as_tree(results, meta, query)
console.print(
Panel(
Markdown(f"```\n{tree_str}\n```", code_theme=CODE_THEME),
title="[dim]Result[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
return
# Handle old format: direct array (backward compatibility)
elif isinstance(parsed_json, list):
tree_str = _format_search_tools_as_tree(parsed_json, None, query)
console.print(
Panel(
Markdown(f"```\n{tree_str}\n```", code_theme=CODE_THEME),
title="[dim]Result[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
return
if tool_name == "execute_code" and isinstance(parsed_json, dict):
execution_time = parsed_json.get("execution_time")
time_str = f" [dim]⏱️ {execution_time:.3f}s[/dim]" if execution_time is not None else ""
has_logs = "logs" in parsed_json and parsed_json["logs"]
has_result = "result" in parsed_json and parsed_json["result"] is not None
if has_result:
# Add execution time to Result only if there are no logs (logs will get the time)
result_title = f"[dim]Result[/dim]{time_str}" if not has_logs else "[dim]Result[/dim]"
console.print(
Panel(
_render_content(parsed_json["result"]),
title=result_title,
border_style="dim white",
padding=(0, 1),
)
)
if has_logs:
# Add execution time to Logs panel (last panel)
console.print(
Panel(
_render_content(parsed_json["logs"]),
title=f"[dim]Logs[/dim]{time_str}",
border_style="dim white",
padding=(0, 1),
)
)
if "error" in parsed_json and parsed_json["error"] is not None:
console.print(
Panel(
_render_content(parsed_json["error"]),
title="[dim red]Error[/dim red]",
border_style="red",
padding=(0, 1),
)
)
else:
console.print(
Panel(
_render_content(parsed_json),
title="[dim]Result[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
else:
console.print(
Panel(
_render_content(output_str),
title="[dim]Result[/dim]",
border_style="dim white",
padding=(0, 1),
)
)
# Handle chat model streaming text chunks
elif event_type == "on_chat_model_stream":
chunk_obj = event_data.get("chunk")
if chunk_obj is None:
return
# Extract text from AIMessageChunk content
text_parts = []
# Try to get content from chunk object
content = chunk_obj.content if hasattr(chunk_obj, "content") else None
# It is unbelievable, but content can be a dict or a string depending on the model.
if content is not None:
if isinstance(content, str):
text_parts.append(content)
else:
item: dict
for item in content:
# Extract text from various possible fields
if "text" in item and item.get("type") != "input_json_delta":
text_parts.append(item["text"])
text = "".join(text_parts)
if text:
# Render text as markdown with custom code block renderer
print(text, end="", flush=True)
# console.print(Markdown(text, code_theme=CODE_THEME), end="\r")