fast-agent-mcp 0.2.57__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- fast_agent/__init__.py +127 -0
- fast_agent/agents/__init__.py +36 -0
- {mcp_agent/core → fast_agent/agents}/agent_types.py +2 -1
- fast_agent/agents/llm_agent.py +217 -0
- fast_agent/agents/llm_decorator.py +486 -0
- mcp_agent/agents/base_agent.py → fast_agent/agents/mcp_agent.py +377 -385
- fast_agent/agents/tool_agent.py +168 -0
- {mcp_agent → fast_agent}/agents/workflow/chain_agent.py +43 -33
- {mcp_agent → fast_agent}/agents/workflow/evaluator_optimizer.py +31 -35
- {mcp_agent → fast_agent}/agents/workflow/iterative_planner.py +56 -47
- {mcp_agent → fast_agent}/agents/workflow/orchestrator_models.py +4 -4
- {mcp_agent → fast_agent}/agents/workflow/parallel_agent.py +34 -41
- {mcp_agent → fast_agent}/agents/workflow/router_agent.py +54 -39
- {mcp_agent → fast_agent}/cli/__main__.py +5 -3
- {mcp_agent → fast_agent}/cli/commands/check_config.py +95 -66
- {mcp_agent → fast_agent}/cli/commands/go.py +20 -11
- {mcp_agent → fast_agent}/cli/commands/quickstart.py +4 -4
- {mcp_agent → fast_agent}/cli/commands/server_helpers.py +1 -1
- {mcp_agent → fast_agent}/cli/commands/setup.py +64 -134
- {mcp_agent → fast_agent}/cli/commands/url_parser.py +9 -8
- {mcp_agent → fast_agent}/cli/main.py +36 -16
- {mcp_agent → fast_agent}/cli/terminal.py +2 -2
- {mcp_agent → fast_agent}/config.py +13 -2
- fast_agent/constants.py +8 -0
- {mcp_agent → fast_agent}/context.py +24 -19
- {mcp_agent → fast_agent}/context_dependent.py +9 -5
- fast_agent/core/__init__.py +17 -0
- {mcp_agent → fast_agent}/core/agent_app.py +39 -36
- fast_agent/core/core_app.py +135 -0
- {mcp_agent → fast_agent}/core/direct_decorators.py +12 -26
- {mcp_agent → fast_agent}/core/direct_factory.py +95 -73
- {mcp_agent → fast_agent/core}/executor/executor.py +4 -5
- {mcp_agent → fast_agent}/core/fastagent.py +32 -32
- fast_agent/core/logging/__init__.py +5 -0
- {mcp_agent → fast_agent/core}/logging/events.py +3 -3
- {mcp_agent → fast_agent/core}/logging/json_serializer.py +1 -1
- {mcp_agent → fast_agent/core}/logging/listeners.py +85 -7
- {mcp_agent → fast_agent/core}/logging/logger.py +7 -7
- {mcp_agent → fast_agent/core}/logging/transport.py +10 -11
- fast_agent/core/prompt.py +9 -0
- {mcp_agent → fast_agent}/core/validation.py +4 -4
- fast_agent/event_progress.py +61 -0
- fast_agent/history/history_exporter.py +44 -0
- {mcp_agent → fast_agent}/human_input/__init__.py +9 -12
- {mcp_agent → fast_agent}/human_input/elicitation_handler.py +26 -8
- {mcp_agent → fast_agent}/human_input/elicitation_state.py +7 -7
- {mcp_agent → fast_agent}/human_input/simple_form.py +6 -4
- {mcp_agent → fast_agent}/human_input/types.py +1 -18
- fast_agent/interfaces.py +228 -0
- fast_agent/llm/__init__.py +9 -0
- mcp_agent/llm/augmented_llm.py → fast_agent/llm/fastagent_llm.py +128 -218
- fast_agent/llm/internal/passthrough.py +137 -0
- mcp_agent/llm/augmented_llm_playback.py → fast_agent/llm/internal/playback.py +29 -25
- mcp_agent/llm/augmented_llm_silent.py → fast_agent/llm/internal/silent.py +10 -17
- fast_agent/llm/internal/slow.py +38 -0
- {mcp_agent → fast_agent}/llm/memory.py +40 -30
- {mcp_agent → fast_agent}/llm/model_database.py +35 -2
- {mcp_agent → fast_agent}/llm/model_factory.py +103 -77
- fast_agent/llm/model_info.py +126 -0
- {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/anthropic_utils.py +7 -7
- fast_agent/llm/provider/anthropic/llm_anthropic.py +603 -0
- {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/multipart_converter_anthropic.py +79 -86
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2192 -0
- {mcp_agent/llm/providers → fast_agent/llm/provider/google}/google_converter.py +66 -14
- fast_agent/llm/provider/google/llm_google_native.py +431 -0
- mcp_agent/llm/providers/augmented_llm_aliyun.py → fast_agent/llm/provider/openai/llm_aliyun.py +6 -7
- mcp_agent/llm/providers/augmented_llm_azure.py → fast_agent/llm/provider/openai/llm_azure.py +4 -4
- mcp_agent/llm/providers/augmented_llm_deepseek.py → fast_agent/llm/provider/openai/llm_deepseek.py +10 -11
- mcp_agent/llm/providers/augmented_llm_generic.py → fast_agent/llm/provider/openai/llm_generic.py +4 -4
- mcp_agent/llm/providers/augmented_llm_google_oai.py → fast_agent/llm/provider/openai/llm_google_oai.py +4 -4
- mcp_agent/llm/providers/augmented_llm_groq.py → fast_agent/llm/provider/openai/llm_groq.py +14 -16
- mcp_agent/llm/providers/augmented_llm_openai.py → fast_agent/llm/provider/openai/llm_openai.py +133 -206
- mcp_agent/llm/providers/augmented_llm_openrouter.py → fast_agent/llm/provider/openai/llm_openrouter.py +6 -6
- mcp_agent/llm/providers/augmented_llm_tensorzero_openai.py → fast_agent/llm/provider/openai/llm_tensorzero_openai.py +17 -16
- mcp_agent/llm/providers/augmented_llm_xai.py → fast_agent/llm/provider/openai/llm_xai.py +6 -6
- {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/multipart_converter_openai.py +125 -63
- {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_multipart.py +12 -12
- {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_utils.py +18 -16
- {mcp_agent → fast_agent}/llm/provider_key_manager.py +2 -2
- {mcp_agent → fast_agent}/llm/provider_types.py +2 -0
- {mcp_agent → fast_agent}/llm/sampling_converter.py +15 -12
- {mcp_agent → fast_agent}/llm/usage_tracking.py +23 -5
- fast_agent/mcp/__init__.py +43 -0
- {mcp_agent → fast_agent}/mcp/elicitation_factory.py +3 -3
- {mcp_agent → fast_agent}/mcp/elicitation_handlers.py +19 -10
- {mcp_agent → fast_agent}/mcp/gen_client.py +3 -3
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +183 -0
- {mcp_agent → fast_agent}/mcp/helpers/server_config_helpers.py +8 -8
- {mcp_agent → fast_agent}/mcp/hf_auth.py +25 -23
- fast_agent/mcp/interfaces.py +93 -0
- {mcp_agent → fast_agent}/mcp/logger_textio.py +4 -4
- {mcp_agent → fast_agent}/mcp/mcp_agent_client_session.py +49 -44
- {mcp_agent → fast_agent}/mcp/mcp_aggregator.py +66 -115
- {mcp_agent → fast_agent}/mcp/mcp_connection_manager.py +16 -23
- {mcp_agent/core → fast_agent/mcp}/mcp_content.py +23 -15
- {mcp_agent → fast_agent}/mcp/mime_utils.py +39 -0
- fast_agent/mcp/prompt.py +159 -0
- mcp_agent/mcp/prompt_message_multipart.py → fast_agent/mcp/prompt_message_extended.py +27 -20
- {mcp_agent → fast_agent}/mcp/prompt_render.py +21 -19
- {mcp_agent → fast_agent}/mcp/prompt_serialization.py +46 -46
- fast_agent/mcp/prompts/__main__.py +7 -0
- {mcp_agent → fast_agent}/mcp/prompts/prompt_helpers.py +31 -30
- {mcp_agent → fast_agent}/mcp/prompts/prompt_load.py +8 -8
- {mcp_agent → fast_agent}/mcp/prompts/prompt_server.py +11 -19
- {mcp_agent → fast_agent}/mcp/prompts/prompt_template.py +18 -18
- {mcp_agent → fast_agent}/mcp/resource_utils.py +1 -1
- {mcp_agent → fast_agent}/mcp/sampling.py +31 -26
- {mcp_agent/mcp_server → fast_agent/mcp/server}/__init__.py +1 -1
- {mcp_agent/mcp_server → fast_agent/mcp/server}/agent_server.py +5 -6
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +90 -0
- {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis-campaign.py +5 -4
- {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_forms_server.py +25 -3
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/forms_demo.py +3 -3
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character.py +2 -2
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character_handler.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/tool_call.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_one.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_two.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/researcher/researcher-eval.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/researcher/researcher-imp.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/researcher/researcher.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/tensorzero/agent.py +2 -2
- {mcp_agent → fast_agent}/resources/examples/tensorzero/image_demo.py +3 -3
- {mcp_agent → fast_agent}/resources/examples/tensorzero/simple_agent.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/workflows/chaining.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/workflows/evaluator.py +3 -3
- {mcp_agent → fast_agent}/resources/examples/workflows/human_input.py +5 -3
- {mcp_agent → fast_agent}/resources/examples/workflows/orchestrator.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/workflows/parallel.py +2 -2
- {mcp_agent → fast_agent}/resources/examples/workflows/router.py +5 -2
- fast_agent/resources/setup/.gitignore +24 -0
- fast_agent/resources/setup/agent.py +18 -0
- fast_agent/resources/setup/fastagent.config.yaml +44 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/types/__init__.py +32 -0
- fast_agent/types/llm_stop_reason.py +77 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console_display.py +1005 -0
- {mcp_agent/human_input → fast_agent/ui}/elicitation_form.py +56 -39
- mcp_agent/human_input/elicitation_forms.py → fast_agent/ui/elicitation_style.py +1 -1
- {mcp_agent/core → fast_agent/ui}/enhanced_prompt.py +96 -25
- {mcp_agent/core → fast_agent/ui}/interactive_prompt.py +330 -125
- fast_agent/ui/mcp_ui_utils.py +224 -0
- {mcp_agent → fast_agent/ui}/progress_display.py +2 -2
- {mcp_agent/logging → fast_agent/ui}/rich_progress.py +4 -4
- {mcp_agent/core → fast_agent/ui}/usage_display.py +3 -8
- {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/METADATA +7 -7
- fast_agent_mcp-0.3.0.dist-info/RECORD +202 -0
- fast_agent_mcp-0.3.0.dist-info/entry_points.txt +5 -0
- fast_agent_mcp-0.2.57.dist-info/RECORD +0 -192
- fast_agent_mcp-0.2.57.dist-info/entry_points.txt +0 -6
- mcp_agent/__init__.py +0 -114
- mcp_agent/agents/agent.py +0 -92
- mcp_agent/agents/workflow/__init__.py +0 -1
- mcp_agent/agents/workflow/orchestrator_agent.py +0 -597
- mcp_agent/app.py +0 -175
- mcp_agent/core/__init__.py +0 -26
- mcp_agent/core/prompt.py +0 -191
- mcp_agent/event_progress.py +0 -134
- mcp_agent/human_input/handler.py +0 -81
- mcp_agent/llm/__init__.py +0 -2
- mcp_agent/llm/augmented_llm_passthrough.py +0 -232
- mcp_agent/llm/augmented_llm_slow.py +0 -53
- mcp_agent/llm/providers/__init__.py +0 -8
- mcp_agent/llm/providers/augmented_llm_anthropic.py +0 -717
- mcp_agent/llm/providers/augmented_llm_bedrock.py +0 -1788
- mcp_agent/llm/providers/augmented_llm_google_native.py +0 -495
- mcp_agent/llm/providers/sampling_converter_anthropic.py +0 -57
- mcp_agent/llm/providers/sampling_converter_openai.py +0 -26
- mcp_agent/llm/sampling_format_converter.py +0 -37
- mcp_agent/logging/__init__.py +0 -0
- mcp_agent/mcp/__init__.py +0 -50
- mcp_agent/mcp/helpers/__init__.py +0 -25
- mcp_agent/mcp/helpers/content_helpers.py +0 -187
- mcp_agent/mcp/interfaces.py +0 -266
- mcp_agent/mcp/prompts/__init__.py +0 -0
- mcp_agent/mcp/prompts/__main__.py +0 -10
- mcp_agent/mcp_server_registry.py +0 -343
- mcp_agent/tools/tool_definition.py +0 -14
- mcp_agent/ui/console_display.py +0 -790
- mcp_agent/ui/console_display_legacy.py +0 -401
- {mcp_agent → fast_agent}/agents/workflow/orchestrator_prompts.py +0 -0
- {mcp_agent/agents → fast_agent/cli}/__init__.py +0 -0
- {mcp_agent → fast_agent}/cli/constants.py +0 -0
- {mcp_agent → fast_agent}/core/error_handling.py +0 -0
- {mcp_agent → fast_agent}/core/exceptions.py +0 -0
- {mcp_agent/cli → fast_agent/core/executor}/__init__.py +0 -0
- {mcp_agent → fast_agent/core}/executor/task_registry.py +0 -0
- {mcp_agent → fast_agent/core}/executor/workflow_signal.py +0 -0
- {mcp_agent → fast_agent}/human_input/form_fields.py +0 -0
- {mcp_agent → fast_agent}/llm/prompt_utils.py +0 -0
- {mcp_agent/core → fast_agent/llm}/request_params.py +0 -0
- {mcp_agent → fast_agent}/mcp/common.py +0 -0
- {mcp_agent/executor → fast_agent/mcp/prompts}/__init__.py +0 -0
- {mcp_agent → fast_agent}/mcp/prompts/prompt_constants.py +0 -0
- {mcp_agent → fast_agent}/py.typed +0 -0
- {mcp_agent → fast_agent}/resources/examples/data-analysis/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_account_server.py +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_game_server.py +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +0 -0
- {mcp_agent → fast_agent}/resources/examples/researcher/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/.env.sample +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/Makefile +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/README.md +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/crab.png +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/docker-compose.yml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/Dockerfile +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/entrypoint.sh +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/mcp_server.py +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/pyproject.toml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_schema.json +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/graded_report.md +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/short_story.md +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/short_story.txt +0 -0
- {mcp_agent → fast_agent/ui}/console.py +0 -0
- {mcp_agent/core → fast_agent/ui}/mermaid_utils.py +0 -0
- {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from json import JSONDecodeError
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from mcp.types import CallToolResult
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from fast_agent.ui import console
|
|
10
|
+
from fast_agent.ui.mcp_ui_utils import UILink
|
|
11
|
+
from fast_agent.ui.mermaid_utils import (
|
|
12
|
+
MermaidDiagram,
|
|
13
|
+
create_mermaid_live_link,
|
|
14
|
+
detect_diagram_type,
|
|
15
|
+
extract_mermaid_diagrams,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
|
|
20
|
+
|
|
21
|
+
CODE_STYLE = "native"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MessageType(Enum):
|
|
25
|
+
"""Types of messages that can be displayed."""
|
|
26
|
+
|
|
27
|
+
USER = "user"
|
|
28
|
+
ASSISTANT = "assistant"
|
|
29
|
+
TOOL_CALL = "tool_call"
|
|
30
|
+
TOOL_RESULT = "tool_result"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Configuration for each message type
|
|
34
|
+
MESSAGE_CONFIGS = {
|
|
35
|
+
MessageType.USER: {
|
|
36
|
+
"block_color": "blue",
|
|
37
|
+
"arrow": "▶",
|
|
38
|
+
"arrow_style": "dim blue",
|
|
39
|
+
"highlight_color": "blue",
|
|
40
|
+
},
|
|
41
|
+
MessageType.ASSISTANT: {
|
|
42
|
+
"block_color": "green",
|
|
43
|
+
"arrow": "◀",
|
|
44
|
+
"arrow_style": "dim green",
|
|
45
|
+
"highlight_color": "bright_green",
|
|
46
|
+
},
|
|
47
|
+
MessageType.TOOL_CALL: {
|
|
48
|
+
"block_color": "magenta",
|
|
49
|
+
"arrow": "◀",
|
|
50
|
+
"arrow_style": "dim magenta",
|
|
51
|
+
"highlight_color": "magenta",
|
|
52
|
+
},
|
|
53
|
+
MessageType.TOOL_RESULT: {
|
|
54
|
+
"block_color": "magenta", # Can be overridden to red if error
|
|
55
|
+
"arrow": "▶",
|
|
56
|
+
"arrow_style": "dim magenta",
|
|
57
|
+
"highlight_color": "magenta",
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
HTML_ESCAPE_CHARS = {
|
|
62
|
+
"&": "&",
|
|
63
|
+
"<": "<",
|
|
64
|
+
">": ">",
|
|
65
|
+
'"': """,
|
|
66
|
+
"'": "'",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _prepare_markdown_content(content: str, escape_xml: bool = True) -> str:
|
|
71
|
+
"""Prepare content for markdown rendering by escaping HTML/XML tags
|
|
72
|
+
while preserving code blocks and inline code.
|
|
73
|
+
|
|
74
|
+
This ensures XML/HTML tags are displayed as visible text rather than
|
|
75
|
+
being interpreted as markup by the markdown renderer.
|
|
76
|
+
|
|
77
|
+
Note: This method does not handle overlapping code blocks (e.g., if inline
|
|
78
|
+
code appears within a fenced code block range). In practice, this is not
|
|
79
|
+
an issue since markdown syntax doesn't support such overlapping.
|
|
80
|
+
"""
|
|
81
|
+
if not escape_xml or not isinstance(content, str):
|
|
82
|
+
return content
|
|
83
|
+
|
|
84
|
+
protected_ranges = []
|
|
85
|
+
import re
|
|
86
|
+
|
|
87
|
+
# Protect fenced code blocks (don't escape anything inside these)
|
|
88
|
+
code_block_pattern = r"```[\s\S]*?```"
|
|
89
|
+
for match in re.finditer(code_block_pattern, content):
|
|
90
|
+
protected_ranges.append((match.start(), match.end()))
|
|
91
|
+
|
|
92
|
+
# Protect inline code (don't escape anything inside these)
|
|
93
|
+
inline_code_pattern = r"(?<!`)`(?!``)[^`\n]+`(?!`)"
|
|
94
|
+
for match in re.finditer(inline_code_pattern, content):
|
|
95
|
+
protected_ranges.append((match.start(), match.end()))
|
|
96
|
+
|
|
97
|
+
protected_ranges.sort(key=lambda x: x[0])
|
|
98
|
+
|
|
99
|
+
# Build the escaped content
|
|
100
|
+
result = []
|
|
101
|
+
last_end = 0
|
|
102
|
+
|
|
103
|
+
for start, end in protected_ranges:
|
|
104
|
+
# Escape everything outside protected ranges
|
|
105
|
+
unprotected_text = content[last_end:start]
|
|
106
|
+
for char, replacement in HTML_ESCAPE_CHARS.items():
|
|
107
|
+
unprotected_text = unprotected_text.replace(char, replacement)
|
|
108
|
+
result.append(unprotected_text)
|
|
109
|
+
|
|
110
|
+
# Keep protected ranges (code blocks) as-is
|
|
111
|
+
result.append(content[start:end])
|
|
112
|
+
last_end = end
|
|
113
|
+
|
|
114
|
+
# Escape any remaining content after the last protected range
|
|
115
|
+
remainder_text = content[last_end:]
|
|
116
|
+
for char, replacement in HTML_ESCAPE_CHARS.items():
|
|
117
|
+
remainder_text = remainder_text.replace(char, replacement)
|
|
118
|
+
result.append(remainder_text)
|
|
119
|
+
|
|
120
|
+
return "".join(result)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ConsoleDisplay:
|
|
124
|
+
"""
|
|
125
|
+
Handles displaying formatted messages, tool calls, and results to the console.
|
|
126
|
+
This centralizes the UI display logic used by LLM implementations.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, config=None) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Initialize the console display handler.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
config: Configuration object containing display preferences
|
|
135
|
+
"""
|
|
136
|
+
self.config = config
|
|
137
|
+
self._markup = config.logger.enable_markup if config else True
|
|
138
|
+
self._escape_xml = True
|
|
139
|
+
|
|
140
|
+
def display_message(
|
|
141
|
+
self,
|
|
142
|
+
content: Any,
|
|
143
|
+
message_type: MessageType,
|
|
144
|
+
name: str | None = None,
|
|
145
|
+
right_info: str = "",
|
|
146
|
+
bottom_metadata: List[str] | None = None,
|
|
147
|
+
highlight_items: str | List[str] | None = None,
|
|
148
|
+
max_item_length: int | None = None,
|
|
149
|
+
is_error: bool = False,
|
|
150
|
+
truncate_content: bool = True,
|
|
151
|
+
additional_message: Text | None = None,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Unified method to display formatted messages to the console.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
content: The main content to display (str, Text, JSON, etc.)
|
|
158
|
+
message_type: Type of message (USER, ASSISTANT, TOOL_CALL, TOOL_RESULT)
|
|
159
|
+
name: Optional name to display (agent name, user name, etc.)
|
|
160
|
+
right_info: Information to display on the right side of the header
|
|
161
|
+
bottom_metadata: Optional list of items for bottom separator
|
|
162
|
+
highlight_items: Item(s) to highlight in bottom metadata
|
|
163
|
+
max_item_length: Optional max length for bottom metadata items (with ellipsis)
|
|
164
|
+
is_error: For tool results, whether this is an error (uses red color)
|
|
165
|
+
truncate_content: Whether to truncate long content
|
|
166
|
+
"""
|
|
167
|
+
# Get configuration for this message type
|
|
168
|
+
config = MESSAGE_CONFIGS[message_type]
|
|
169
|
+
|
|
170
|
+
# Override colors for error states
|
|
171
|
+
if is_error and message_type == MessageType.TOOL_RESULT:
|
|
172
|
+
block_color = "red"
|
|
173
|
+
else:
|
|
174
|
+
block_color = config["block_color"]
|
|
175
|
+
|
|
176
|
+
# Build the left side of the header
|
|
177
|
+
arrow = config["arrow"]
|
|
178
|
+
arrow_style = config["arrow_style"]
|
|
179
|
+
left = f"[{block_color}]▎[/{block_color}][{arrow_style}]{arrow}[/{arrow_style}]"
|
|
180
|
+
if name:
|
|
181
|
+
left += f" [{block_color if not is_error else 'red'}]{name}[/{block_color if not is_error else 'red'}]"
|
|
182
|
+
|
|
183
|
+
# Create combined separator and status line
|
|
184
|
+
self._create_combined_separator_status(left, right_info)
|
|
185
|
+
|
|
186
|
+
# Display the content
|
|
187
|
+
self._display_content(
|
|
188
|
+
content, truncate_content, is_error, message_type, check_markdown_markers=False
|
|
189
|
+
)
|
|
190
|
+
if additional_message:
|
|
191
|
+
console.console.print(additional_message, markup=self._markup)
|
|
192
|
+
|
|
193
|
+
# Handle bottom separator with optional metadata
|
|
194
|
+
console.console.print()
|
|
195
|
+
|
|
196
|
+
if bottom_metadata:
|
|
197
|
+
# Apply shortening if requested
|
|
198
|
+
display_items = bottom_metadata
|
|
199
|
+
if max_item_length:
|
|
200
|
+
display_items = self._shorten_items(bottom_metadata, max_item_length)
|
|
201
|
+
|
|
202
|
+
# Normalize highlight_items
|
|
203
|
+
if highlight_items is None:
|
|
204
|
+
highlight_items = []
|
|
205
|
+
elif isinstance(highlight_items, str):
|
|
206
|
+
highlight_items = [highlight_items]
|
|
207
|
+
|
|
208
|
+
# Shorten highlight items to match if we shortened display items
|
|
209
|
+
if max_item_length:
|
|
210
|
+
highlight_items = self._shorten_items(highlight_items, max_item_length)
|
|
211
|
+
|
|
212
|
+
# Format the metadata with highlighting, clipped to available width
|
|
213
|
+
# Compute available width for the metadata segment (excluding the fixed prefix/suffix)
|
|
214
|
+
total_width = console.console.size.width
|
|
215
|
+
prefix = Text("─| ")
|
|
216
|
+
prefix.stylize("dim")
|
|
217
|
+
suffix = Text(" |")
|
|
218
|
+
suffix.stylize("dim")
|
|
219
|
+
available = max(0, total_width - prefix.cell_len - suffix.cell_len)
|
|
220
|
+
|
|
221
|
+
metadata_text = self._format_bottom_metadata(
|
|
222
|
+
display_items,
|
|
223
|
+
highlight_items,
|
|
224
|
+
config["highlight_color"],
|
|
225
|
+
max_width=available,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Create the separator line with metadata
|
|
229
|
+
line = Text()
|
|
230
|
+
line.append_text(prefix)
|
|
231
|
+
line.append_text(metadata_text)
|
|
232
|
+
line.append_text(suffix)
|
|
233
|
+
remaining = total_width - line.cell_len
|
|
234
|
+
if remaining > 0:
|
|
235
|
+
line.append("─" * remaining, style="dim")
|
|
236
|
+
console.console.print(line, markup=self._markup)
|
|
237
|
+
else:
|
|
238
|
+
# No metadata - continuous bar
|
|
239
|
+
console.console.print("─" * console.console.size.width, style="dim")
|
|
240
|
+
|
|
241
|
+
console.console.print()
|
|
242
|
+
|
|
243
|
+
def _display_content(
|
|
244
|
+
self,
|
|
245
|
+
content: Any,
|
|
246
|
+
truncate: bool = True,
|
|
247
|
+
is_error: bool = False,
|
|
248
|
+
message_type: Optional[MessageType] = None,
|
|
249
|
+
check_markdown_markers: bool = False,
|
|
250
|
+
) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Display content in the appropriate format.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
content: Content to display
|
|
256
|
+
truncate: Whether to truncate long content
|
|
257
|
+
is_error: Whether this is error content (affects styling)
|
|
258
|
+
message_type: Type of message to determine appropriate styling
|
|
259
|
+
check_markdown_markers: If True, only use markdown rendering when markers are present
|
|
260
|
+
"""
|
|
261
|
+
import json
|
|
262
|
+
import re
|
|
263
|
+
|
|
264
|
+
from rich.markdown import Markdown
|
|
265
|
+
from rich.pretty import Pretty
|
|
266
|
+
from rich.syntax import Syntax
|
|
267
|
+
|
|
268
|
+
from fast_agent.mcp.helpers.content_helpers import get_text, is_text_content
|
|
269
|
+
|
|
270
|
+
# Determine the style based on message type
|
|
271
|
+
# USER and ASSISTANT messages should display in normal style
|
|
272
|
+
# TOOL_CALL and TOOL_RESULT should be dimmed
|
|
273
|
+
if is_error:
|
|
274
|
+
style = "dim red"
|
|
275
|
+
elif message_type in [MessageType.USER, MessageType.ASSISTANT]:
|
|
276
|
+
style = None # No style means default/normal white
|
|
277
|
+
else:
|
|
278
|
+
style = "dim"
|
|
279
|
+
|
|
280
|
+
# Handle different content types
|
|
281
|
+
if isinstance(content, str):
|
|
282
|
+
# Try to detect and handle different string formats
|
|
283
|
+
try:
|
|
284
|
+
# Try as JSON first
|
|
285
|
+
json_obj = json.loads(content)
|
|
286
|
+
if truncate and self.config and self.config.logger.truncate_tools:
|
|
287
|
+
pretty_obj = Pretty(json_obj, max_length=10, max_string=50)
|
|
288
|
+
else:
|
|
289
|
+
pretty_obj = Pretty(json_obj)
|
|
290
|
+
# Apply style only if specified
|
|
291
|
+
if style:
|
|
292
|
+
console.console.print(pretty_obj, style=style, markup=self._markup)
|
|
293
|
+
else:
|
|
294
|
+
console.console.print(pretty_obj, markup=self._markup)
|
|
295
|
+
except (JSONDecodeError, TypeError, ValueError):
|
|
296
|
+
# Check if content appears to be primarily XML
|
|
297
|
+
xml_pattern = r"^<[a-zA-Z_][a-zA-Z0-9_-]*[^>]*>"
|
|
298
|
+
is_xml_content = (
|
|
299
|
+
bool(re.match(xml_pattern, content.strip())) and content.count("<") > 5
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if is_xml_content:
|
|
303
|
+
# Display XML content with syntax highlighting for better readability
|
|
304
|
+
syntax = Syntax(content, "xml", theme=CODE_STYLE, line_numbers=False)
|
|
305
|
+
console.console.print(syntax, markup=self._markup)
|
|
306
|
+
elif check_markdown_markers:
|
|
307
|
+
# Check for markdown markers before deciding to use markdown rendering
|
|
308
|
+
if any(marker in content for marker in ["##", "**", "*", "`", "---", "###"]):
|
|
309
|
+
# Has markdown markers - render as markdown with escaping
|
|
310
|
+
prepared_content = _prepare_markdown_content(content, self._escape_xml)
|
|
311
|
+
md = Markdown(prepared_content, code_theme=CODE_STYLE)
|
|
312
|
+
console.console.print(md, markup=self._markup)
|
|
313
|
+
else:
|
|
314
|
+
# Plain text - display as-is
|
|
315
|
+
if (
|
|
316
|
+
truncate
|
|
317
|
+
and self.config
|
|
318
|
+
and self.config.logger.truncate_tools
|
|
319
|
+
and len(content) > 360
|
|
320
|
+
):
|
|
321
|
+
content = content[:360] + "..."
|
|
322
|
+
if style:
|
|
323
|
+
console.console.print(content, style=style, markup=self._markup)
|
|
324
|
+
else:
|
|
325
|
+
console.console.print(content, markup=self._markup)
|
|
326
|
+
else:
|
|
327
|
+
# Check if it looks like markdown
|
|
328
|
+
if any(marker in content for marker in ["##", "**", "*", "`", "---", "###"]):
|
|
329
|
+
# Escape HTML/XML tags while preserving code blocks
|
|
330
|
+
prepared_content = _prepare_markdown_content(content, self._escape_xml)
|
|
331
|
+
md = Markdown(prepared_content, code_theme=CODE_STYLE)
|
|
332
|
+
# Markdown handles its own styling, don't apply style
|
|
333
|
+
console.console.print(md, markup=self._markup)
|
|
334
|
+
else:
|
|
335
|
+
# Plain text
|
|
336
|
+
if (
|
|
337
|
+
truncate
|
|
338
|
+
and self.config
|
|
339
|
+
and self.config.logger.truncate_tools
|
|
340
|
+
and len(content) > 360
|
|
341
|
+
):
|
|
342
|
+
content = content[:360] + "..."
|
|
343
|
+
# Apply style only if specified (None means default white)
|
|
344
|
+
if style:
|
|
345
|
+
console.console.print(content, style=style, markup=self._markup)
|
|
346
|
+
else:
|
|
347
|
+
console.console.print(content, markup=self._markup)
|
|
348
|
+
elif isinstance(content, Text):
|
|
349
|
+
# Rich Text object - check if it contains markdown
|
|
350
|
+
plain_text = content.plain
|
|
351
|
+
|
|
352
|
+
# Check if the plain text contains markdown markers
|
|
353
|
+
if any(marker in plain_text for marker in ["##", "**", "*", "`", "---", "###"]):
|
|
354
|
+
# Split the Text object into segments
|
|
355
|
+
# We need to handle the main content (which may have markdown)
|
|
356
|
+
# and any styled segments that were appended
|
|
357
|
+
|
|
358
|
+
# For now, we'll render the entire content with markdown support
|
|
359
|
+
# This means extracting each span and handling it appropriately
|
|
360
|
+
from rich.markdown import Markdown
|
|
361
|
+
|
|
362
|
+
# If the Text object has multiple spans with different styles,
|
|
363
|
+
# we need to be careful about how we render them
|
|
364
|
+
if len(content._spans) > 1:
|
|
365
|
+
# Complex case: Text has multiple styled segments
|
|
366
|
+
# We'll render the first part as markdown if it contains markers
|
|
367
|
+
# and append other styled parts separately
|
|
368
|
+
|
|
369
|
+
# Find where the markdown content ends (usually the first span)
|
|
370
|
+
markdown_end = content._spans[0].end if content._spans else len(plain_text)
|
|
371
|
+
markdown_part = plain_text[:markdown_end]
|
|
372
|
+
|
|
373
|
+
# Check if the first part has markdown
|
|
374
|
+
if any(
|
|
375
|
+
marker in markdown_part for marker in ["##", "**", "*", "`", "---", "###"]
|
|
376
|
+
):
|
|
377
|
+
# Render markdown part
|
|
378
|
+
prepared_content = _prepare_markdown_content(
|
|
379
|
+
markdown_part, self._escape_xml
|
|
380
|
+
)
|
|
381
|
+
md = Markdown(prepared_content, code_theme=CODE_STYLE)
|
|
382
|
+
console.console.print(md, markup=self._markup)
|
|
383
|
+
|
|
384
|
+
# Then render any additional styled segments
|
|
385
|
+
if markdown_end < len(plain_text):
|
|
386
|
+
remaining_text = Text()
|
|
387
|
+
for span in content._spans:
|
|
388
|
+
if span.start >= markdown_end:
|
|
389
|
+
segment_text = plain_text[span.start : span.end]
|
|
390
|
+
remaining_text.append(segment_text, style=span.style)
|
|
391
|
+
if remaining_text.plain:
|
|
392
|
+
console.console.print(remaining_text, markup=self._markup)
|
|
393
|
+
else:
|
|
394
|
+
# No markdown in first part, just print the whole Text object
|
|
395
|
+
console.console.print(content, markup=self._markup)
|
|
396
|
+
else:
|
|
397
|
+
# Simple case: entire text should be rendered as markdown
|
|
398
|
+
prepared_content = _prepare_markdown_content(plain_text, self._escape_xml)
|
|
399
|
+
md = Markdown(prepared_content, code_theme=CODE_STYLE)
|
|
400
|
+
console.console.print(md, markup=self._markup)
|
|
401
|
+
else:
|
|
402
|
+
# No markdown markers, print as regular Rich Text
|
|
403
|
+
console.console.print(content, markup=self._markup)
|
|
404
|
+
elif isinstance(content, list):
|
|
405
|
+
# Handle content blocks (for tool results)
|
|
406
|
+
if len(content) == 1 and is_text_content(content[0]):
|
|
407
|
+
# Single text block - display directly
|
|
408
|
+
text_content = get_text(content[0])
|
|
409
|
+
if text_content:
|
|
410
|
+
if (
|
|
411
|
+
truncate
|
|
412
|
+
and self.config
|
|
413
|
+
and self.config.logger.truncate_tools
|
|
414
|
+
and len(text_content) > 360
|
|
415
|
+
):
|
|
416
|
+
text_content = text_content[:360] + "..."
|
|
417
|
+
# Apply style only if specified
|
|
418
|
+
if style:
|
|
419
|
+
console.console.print(text_content, style=style, markup=self._markup)
|
|
420
|
+
else:
|
|
421
|
+
console.console.print(text_content, markup=self._markup)
|
|
422
|
+
else:
|
|
423
|
+
# Apply style only if specified
|
|
424
|
+
if style:
|
|
425
|
+
console.console.print("(empty text)", style=style, markup=self._markup)
|
|
426
|
+
else:
|
|
427
|
+
console.console.print("(empty text)", markup=self._markup)
|
|
428
|
+
else:
|
|
429
|
+
# Multiple blocks or non-text content
|
|
430
|
+
if truncate and self.config and self.config.logger.truncate_tools:
|
|
431
|
+
pretty_obj = Pretty(content, max_length=10, max_string=50)
|
|
432
|
+
else:
|
|
433
|
+
pretty_obj = Pretty(content)
|
|
434
|
+
# Apply style only if specified
|
|
435
|
+
if style:
|
|
436
|
+
console.console.print(pretty_obj, style=style, markup=self._markup)
|
|
437
|
+
else:
|
|
438
|
+
console.console.print(pretty_obj, markup=self._markup)
|
|
439
|
+
else:
|
|
440
|
+
# Any other type - use Pretty
|
|
441
|
+
if truncate and self.config and self.config.logger.truncate_tools:
|
|
442
|
+
pretty_obj = Pretty(content, max_length=10, max_string=50)
|
|
443
|
+
else:
|
|
444
|
+
pretty_obj = Pretty(content)
|
|
445
|
+
# Apply style only if specified
|
|
446
|
+
if style:
|
|
447
|
+
console.console.print(pretty_obj, style=style, markup=self._markup)
|
|
448
|
+
else:
|
|
449
|
+
console.console.print(pretty_obj, markup=self._markup)
|
|
450
|
+
|
|
451
|
+
def _shorten_items(self, items: List[str], max_length: int) -> List[str]:
|
|
452
|
+
"""
|
|
453
|
+
Shorten items to max_length with ellipsis if needed.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
items: List of strings to potentially shorten
|
|
457
|
+
max_length: Maximum length for each item
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
List of shortened strings
|
|
461
|
+
"""
|
|
462
|
+
return [item[: max_length - 1] + "…" if len(item) > max_length else item for item in items]
|
|
463
|
+
|
|
464
|
+
def _format_bottom_metadata(
|
|
465
|
+
self,
|
|
466
|
+
items: List[str],
|
|
467
|
+
highlight_items: List[str],
|
|
468
|
+
highlight_color: str,
|
|
469
|
+
max_width: int | None = None,
|
|
470
|
+
) -> Text:
|
|
471
|
+
"""
|
|
472
|
+
Format a list of items with pipe separators and highlighting.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
items: List of items to display
|
|
476
|
+
highlight_items: List of items to highlight
|
|
477
|
+
highlight_color: Color to use for highlighting
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Formatted Text object with proper separators and highlighting
|
|
481
|
+
"""
|
|
482
|
+
formatted = Text()
|
|
483
|
+
|
|
484
|
+
def will_fit(next_segment: Text) -> bool:
|
|
485
|
+
if max_width is None:
|
|
486
|
+
return True
|
|
487
|
+
# projected length if we append next_segment
|
|
488
|
+
return formatted.cell_len + next_segment.cell_len <= max_width
|
|
489
|
+
|
|
490
|
+
for i, item in enumerate(items):
|
|
491
|
+
sep = Text(" | ", style="dim") if i > 0 else Text("")
|
|
492
|
+
|
|
493
|
+
# Prepare item text with potential highlighting
|
|
494
|
+
should_highlight = False
|
|
495
|
+
if item in highlight_items:
|
|
496
|
+
should_highlight = True
|
|
497
|
+
else:
|
|
498
|
+
for highlight in highlight_items:
|
|
499
|
+
if item.startswith(highlight) or highlight.endswith(item):
|
|
500
|
+
should_highlight = True
|
|
501
|
+
break
|
|
502
|
+
|
|
503
|
+
item_text = Text(item, style=(highlight_color if should_highlight else "dim"))
|
|
504
|
+
|
|
505
|
+
# Check if separator + item fits in available width
|
|
506
|
+
if not will_fit(sep + item_text):
|
|
507
|
+
# If nothing has been added yet and the item itself is too long,
|
|
508
|
+
# leave space for an ellipsis and stop.
|
|
509
|
+
if formatted.cell_len == 0 and max_width is not None and max_width > 1:
|
|
510
|
+
# show truncated indicator only
|
|
511
|
+
formatted.append("…", style="dim")
|
|
512
|
+
else:
|
|
513
|
+
# Indicate there are more items but avoid wrapping
|
|
514
|
+
if max_width is None or formatted.cell_len < max_width:
|
|
515
|
+
formatted.append(" …", style="dim")
|
|
516
|
+
break
|
|
517
|
+
|
|
518
|
+
# Append separator and item
|
|
519
|
+
if sep.plain:
|
|
520
|
+
formatted.append_text(sep)
|
|
521
|
+
formatted.append_text(item_text)
|
|
522
|
+
|
|
523
|
+
return formatted
|
|
524
|
+
|
|
525
|
+
def show_tool_result(self, result: CallToolResult, name: str | None = None) -> None:
|
|
526
|
+
"""Display a tool result in the new visual style."""
|
|
527
|
+
if not self.config or not self.config.logger.show_tools:
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
# Import content helpers
|
|
531
|
+
from fast_agent.mcp.helpers.content_helpers import get_text, is_text_content
|
|
532
|
+
|
|
533
|
+
# Analyze content to determine display format and status
|
|
534
|
+
content = result.content
|
|
535
|
+
if result.isError:
|
|
536
|
+
status = "ERROR"
|
|
537
|
+
else:
|
|
538
|
+
# Check if it's a list with content blocks
|
|
539
|
+
if len(content) == 0:
|
|
540
|
+
status = "No Content"
|
|
541
|
+
elif len(content) == 1 and is_text_content(content[0]):
|
|
542
|
+
text_content = get_text(content[0])
|
|
543
|
+
char_count = len(text_content) if text_content else 0
|
|
544
|
+
status = f"Text Only {char_count} chars"
|
|
545
|
+
else:
|
|
546
|
+
text_count = sum(1 for item in content if is_text_content(item))
|
|
547
|
+
if text_count == len(content):
|
|
548
|
+
status = f"{len(content)} Text Blocks" if len(content) > 1 else "1 Text Block"
|
|
549
|
+
else:
|
|
550
|
+
status = (
|
|
551
|
+
f"{len(content)} Content Blocks" if len(content) > 1 else "1 Content Block"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Build right info
|
|
555
|
+
right_info = f"[dim]tool result - {status}[/dim]"
|
|
556
|
+
|
|
557
|
+
# Display using unified method
|
|
558
|
+
self.display_message(
|
|
559
|
+
content=content,
|
|
560
|
+
message_type=MessageType.TOOL_RESULT,
|
|
561
|
+
name=name,
|
|
562
|
+
right_info=right_info,
|
|
563
|
+
is_error=result.isError,
|
|
564
|
+
truncate_content=True,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
def show_tool_call(
|
|
568
|
+
self,
|
|
569
|
+
tool_name: str,
|
|
570
|
+
tool_args: Dict[str, Any] | None,
|
|
571
|
+
bottom_items: List[str] | None = None,
|
|
572
|
+
highlight_items: str | List[str] | None = None,
|
|
573
|
+
max_item_length: int | None = None,
|
|
574
|
+
name: str | None = None,
|
|
575
|
+
) -> None:
|
|
576
|
+
"""Display a tool call in the new visual style.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
tool_name: Name of the tool being called
|
|
580
|
+
tool_args: Arguments being passed to the tool
|
|
581
|
+
bottom_items: Optional list of items for bottom separator (e.g., available tools)
|
|
582
|
+
highlight_items: Item(s) to highlight in the bottom separator
|
|
583
|
+
max_item_length: Optional max length for bottom items (with ellipsis)
|
|
584
|
+
name: Optional agent name
|
|
585
|
+
"""
|
|
586
|
+
if not self.config or not self.config.logger.show_tools:
|
|
587
|
+
return
|
|
588
|
+
|
|
589
|
+
# Build right info
|
|
590
|
+
right_info = f"[dim]tool request - {tool_name}[/dim]"
|
|
591
|
+
|
|
592
|
+
# Display using unified method
|
|
593
|
+
self.display_message(
|
|
594
|
+
content=tool_args,
|
|
595
|
+
message_type=MessageType.TOOL_CALL,
|
|
596
|
+
name=name,
|
|
597
|
+
right_info=right_info,
|
|
598
|
+
bottom_metadata=bottom_items,
|
|
599
|
+
highlight_items=tool_name,
|
|
600
|
+
max_item_length=max_item_length,
|
|
601
|
+
truncate_content=True,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
async def show_tool_update(self, updated_server: str, agent_name: str | None = None) -> None:
|
|
605
|
+
"""Show a tool update for a server in the new visual style.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
updated_server: Name of the server being updated
|
|
609
|
+
agent_name: Optional agent name to display
|
|
610
|
+
"""
|
|
611
|
+
if not self.config or not self.config.logger.show_tools:
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
# Combined separator and status line
|
|
615
|
+
if agent_name:
|
|
616
|
+
left = (
|
|
617
|
+
f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
|
|
618
|
+
)
|
|
619
|
+
else:
|
|
620
|
+
left = "[magenta]▎[/magenta][dim magenta]▶[/dim magenta]"
|
|
621
|
+
|
|
622
|
+
right = f"[dim]{updated_server}[/dim]"
|
|
623
|
+
self._create_combined_separator_status(left, right)
|
|
624
|
+
|
|
625
|
+
# Display update message
|
|
626
|
+
message = f"Updating tools for server {updated_server}"
|
|
627
|
+
console.console.print(message, style="dim", markup=self._markup)
|
|
628
|
+
|
|
629
|
+
# Bottom separator
|
|
630
|
+
console.console.print()
|
|
631
|
+
console.console.print("─" * console.console.size.width, style="dim")
|
|
632
|
+
console.console.print()
|
|
633
|
+
|
|
634
|
+
# Force prompt_toolkit redraw if active
|
|
635
|
+
try:
|
|
636
|
+
from prompt_toolkit.application.current import get_app
|
|
637
|
+
|
|
638
|
+
get_app().invalidate() # Forces prompt_toolkit to redraw
|
|
639
|
+
except: # noqa: E722
|
|
640
|
+
pass # No active prompt_toolkit session
|
|
641
|
+
|
|
642
|
+
def _create_combined_separator_status(self, left_content: str, right_info: str = "") -> None:
|
|
643
|
+
"""
|
|
644
|
+
Create a combined separator and status line.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
left_content: The main content (block, arrow, name) - left justified with color
|
|
648
|
+
right_info: Supplementary information to show in brackets - right aligned
|
|
649
|
+
"""
|
|
650
|
+
width = console.console.size.width
|
|
651
|
+
|
|
652
|
+
# Create left text
|
|
653
|
+
left_text = Text.from_markup(left_content)
|
|
654
|
+
|
|
655
|
+
# Create right text if we have info
|
|
656
|
+
if right_info and right_info.strip():
|
|
657
|
+
# Add dim brackets around the right info
|
|
658
|
+
right_text = Text()
|
|
659
|
+
right_text.append("[", style="dim")
|
|
660
|
+
right_text.append_text(Text.from_markup(right_info))
|
|
661
|
+
right_text.append("]", style="dim")
|
|
662
|
+
# Calculate separator count
|
|
663
|
+
separator_count = width - left_text.cell_len - right_text.cell_len
|
|
664
|
+
if separator_count < 1:
|
|
665
|
+
separator_count = 1 # Always at least 1 separator
|
|
666
|
+
else:
|
|
667
|
+
right_text = Text("")
|
|
668
|
+
separator_count = width - left_text.cell_len
|
|
669
|
+
|
|
670
|
+
# Build the combined line
|
|
671
|
+
combined = Text()
|
|
672
|
+
combined.append_text(left_text)
|
|
673
|
+
combined.append(" ", style="default")
|
|
674
|
+
combined.append("─" * (separator_count - 1), style="dim")
|
|
675
|
+
combined.append_text(right_text)
|
|
676
|
+
|
|
677
|
+
# Print with empty line before
|
|
678
|
+
console.console.print()
|
|
679
|
+
console.console.print(combined, markup=self._markup)
|
|
680
|
+
console.console.print()
|
|
681
|
+
|
|
682
|
+
async def show_assistant_message(
|
|
683
|
+
self,
|
|
684
|
+
message_text: Union[str, Text, "PromptMessageExtended"],
|
|
685
|
+
bottom_items: List[str] | None = None,
|
|
686
|
+
highlight_items: str | List[str] | None = None,
|
|
687
|
+
max_item_length: int | None = None,
|
|
688
|
+
name: str | None = None,
|
|
689
|
+
model: str | None = None,
|
|
690
|
+
additional_message: Optional[Text] = None,
|
|
691
|
+
) -> None:
|
|
692
|
+
"""Display an assistant message in a formatted panel.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
message_text: The message content to display (str, Text, or PromptMessageExtended)
|
|
696
|
+
bottom_items: Optional list of items for bottom separator (e.g., servers, destinations)
|
|
697
|
+
highlight_items: Item(s) to highlight in the bottom separator
|
|
698
|
+
max_item_length: Optional max length for bottom items (with ellipsis)
|
|
699
|
+
title: Title for the message (default "ASSISTANT")
|
|
700
|
+
name: Optional agent name
|
|
701
|
+
model: Optional model name for right info
|
|
702
|
+
additional_message: Optional additional styled message to append
|
|
703
|
+
"""
|
|
704
|
+
if not self.config or not self.config.logger.show_chat:
|
|
705
|
+
return
|
|
706
|
+
|
|
707
|
+
# Extract text from PromptMessageExtended if needed
|
|
708
|
+
from fast_agent.types import PromptMessageExtended
|
|
709
|
+
|
|
710
|
+
if isinstance(message_text, PromptMessageExtended):
|
|
711
|
+
display_text = message_text.last_text() or ""
|
|
712
|
+
else:
|
|
713
|
+
display_text = message_text
|
|
714
|
+
|
|
715
|
+
# Build right info
|
|
716
|
+
right_info = f"[dim]{model}[/dim]" if model else ""
|
|
717
|
+
|
|
718
|
+
# Display main message using unified method
|
|
719
|
+
self.display_message(
|
|
720
|
+
content=display_text,
|
|
721
|
+
message_type=MessageType.ASSISTANT,
|
|
722
|
+
name=name,
|
|
723
|
+
right_info=right_info,
|
|
724
|
+
bottom_metadata=bottom_items,
|
|
725
|
+
highlight_items=highlight_items,
|
|
726
|
+
max_item_length=max_item_length,
|
|
727
|
+
truncate_content=False, # Assistant messages shouldn't be truncated
|
|
728
|
+
additional_message=additional_message,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
# Handle mermaid diagrams separately (after the main message)
|
|
732
|
+
# Extract plain text for mermaid detection
|
|
733
|
+
plain_text = display_text
|
|
734
|
+
if isinstance(display_text, Text):
|
|
735
|
+
plain_text = display_text.plain
|
|
736
|
+
|
|
737
|
+
if isinstance(plain_text, str):
|
|
738
|
+
diagrams = extract_mermaid_diagrams(plain_text)
|
|
739
|
+
if diagrams:
|
|
740
|
+
self._display_mermaid_diagrams(diagrams)
|
|
741
|
+
|
|
742
|
+
def _display_mermaid_diagrams(self, diagrams: List[MermaidDiagram]) -> None:
|
|
743
|
+
"""Display mermaid diagram links."""
|
|
744
|
+
diagram_content = Text()
|
|
745
|
+
# Add bullet at the beginning
|
|
746
|
+
diagram_content.append("● ", style="dim")
|
|
747
|
+
|
|
748
|
+
for i, diagram in enumerate(diagrams, 1):
|
|
749
|
+
if i > 1:
|
|
750
|
+
diagram_content.append(" • ", style="dim")
|
|
751
|
+
|
|
752
|
+
# Generate URL
|
|
753
|
+
url = create_mermaid_live_link(diagram.content)
|
|
754
|
+
|
|
755
|
+
# Format: "1 - Title" or "1 - Flowchart" or "Diagram 1"
|
|
756
|
+
if diagram.title:
|
|
757
|
+
diagram_content.append(f"{i} - {diagram.title}", style=f"bright_blue link {url}")
|
|
758
|
+
else:
|
|
759
|
+
# Try to detect diagram type, fallback to "Diagram N"
|
|
760
|
+
diagram_type = detect_diagram_type(diagram.content)
|
|
761
|
+
if diagram_type != "Diagram":
|
|
762
|
+
diagram_content.append(f"{i} - {diagram_type}", style=f"bright_blue link {url}")
|
|
763
|
+
else:
|
|
764
|
+
diagram_content.append(f"Diagram {i}", style=f"bright_blue link {url}")
|
|
765
|
+
|
|
766
|
+
# Display diagrams on a simple new line (more space efficient)
|
|
767
|
+
console.console.print()
|
|
768
|
+
console.console.print(diagram_content, markup=self._markup)
|
|
769
|
+
|
|
770
|
+
async def show_mcp_ui_links(self, links: List[UILink]) -> None:
|
|
771
|
+
"""Display MCP-UI links beneath the chat like mermaid links."""
|
|
772
|
+
if not self.config or not self.config.logger.show_chat:
|
|
773
|
+
return
|
|
774
|
+
|
|
775
|
+
if not links:
|
|
776
|
+
return
|
|
777
|
+
|
|
778
|
+
content = Text()
|
|
779
|
+
content.append("● mcp-ui ", style="dim")
|
|
780
|
+
for i, link in enumerate(links, 1):
|
|
781
|
+
if i > 1:
|
|
782
|
+
content.append(" • ", style="dim")
|
|
783
|
+
# Prefer a web-friendly URL (http(s) or data:) if available; fallback to local file
|
|
784
|
+
url = link.web_url if getattr(link, "web_url", None) else f"file://{link.file_path}"
|
|
785
|
+
label = f"{i} - {link.title}"
|
|
786
|
+
content.append(label, style=f"bright_blue link {url}")
|
|
787
|
+
|
|
788
|
+
console.console.print()
|
|
789
|
+
console.console.print(content, markup=self._markup)
|
|
790
|
+
|
|
791
|
+
def show_user_message(
|
|
792
|
+
self,
|
|
793
|
+
message: Union[str, Text],
|
|
794
|
+
model: str | None = None,
|
|
795
|
+
chat_turn: int = 0,
|
|
796
|
+
name: str | None = None,
|
|
797
|
+
) -> None:
|
|
798
|
+
"""Display a user message in the new visual style."""
|
|
799
|
+
if not self.config or not self.config.logger.show_chat:
|
|
800
|
+
return
|
|
801
|
+
|
|
802
|
+
# Build right side with model and turn
|
|
803
|
+
right_parts = []
|
|
804
|
+
if model:
|
|
805
|
+
right_parts.append(model)
|
|
806
|
+
if chat_turn > 0:
|
|
807
|
+
right_parts.append(f"turn {chat_turn}")
|
|
808
|
+
|
|
809
|
+
right_info = f"[dim]{' '.join(right_parts)}[/dim]" if right_parts else ""
|
|
810
|
+
|
|
811
|
+
self.display_message(
|
|
812
|
+
content=message,
|
|
813
|
+
message_type=MessageType.USER,
|
|
814
|
+
name=name,
|
|
815
|
+
right_info=right_info,
|
|
816
|
+
truncate_content=False, # User messages typically shouldn't be truncated
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
async def show_prompt_loaded(
|
|
820
|
+
self,
|
|
821
|
+
prompt_name: str,
|
|
822
|
+
description: Optional[str] = None,
|
|
823
|
+
message_count: int = 0,
|
|
824
|
+
agent_name: Optional[str] = None,
|
|
825
|
+
server_list: List[str] | None = None,
|
|
826
|
+
highlight_server: str | None = None,
|
|
827
|
+
arguments: Optional[dict[str, str]] = None,
|
|
828
|
+
) -> None:
|
|
829
|
+
"""
|
|
830
|
+
Display information about a loaded prompt template.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
prompt_name: The name of the prompt that was loaded
|
|
834
|
+
description: Optional description of the prompt
|
|
835
|
+
message_count: Number of messages added to the conversation history
|
|
836
|
+
agent_name: Name of the agent using the prompt
|
|
837
|
+
server_list: Optional list of servers to display
|
|
838
|
+
highlight_server: Optional server name to highlight
|
|
839
|
+
arguments: Optional dictionary of arguments passed to the prompt template
|
|
840
|
+
"""
|
|
841
|
+
if not self.config or not self.config.logger.show_tools:
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
# Build the server list with highlighting
|
|
845
|
+
display_server_list = Text()
|
|
846
|
+
if server_list:
|
|
847
|
+
for server_name in server_list:
|
|
848
|
+
style = "green" if server_name == highlight_server else "dim white"
|
|
849
|
+
display_server_list.append(f"[{server_name}] ", style)
|
|
850
|
+
|
|
851
|
+
# Create content text
|
|
852
|
+
content = Text()
|
|
853
|
+
messages_phrase = f"Loaded {message_count} message{'s' if message_count != 1 else ''}"
|
|
854
|
+
content.append(f"{messages_phrase} from template ", style="cyan italic")
|
|
855
|
+
content.append(f"'{prompt_name}'", style="cyan bold italic")
|
|
856
|
+
|
|
857
|
+
if agent_name:
|
|
858
|
+
content.append(f" for {agent_name}", style="cyan italic")
|
|
859
|
+
|
|
860
|
+
# Add template arguments if provided
|
|
861
|
+
if arguments:
|
|
862
|
+
content.append("\n\nArguments:", style="cyan")
|
|
863
|
+
for key, value in arguments.items():
|
|
864
|
+
content.append(f"\n {key}: ", style="cyan bold")
|
|
865
|
+
content.append(value, style="white")
|
|
866
|
+
|
|
867
|
+
if description:
|
|
868
|
+
content.append("\n\n", style="default")
|
|
869
|
+
content.append(description, style="dim white")
|
|
870
|
+
|
|
871
|
+
# Create panel
|
|
872
|
+
panel = Panel(
|
|
873
|
+
content,
|
|
874
|
+
title="[PROMPT LOADED]",
|
|
875
|
+
title_align="right",
|
|
876
|
+
style="cyan",
|
|
877
|
+
border_style="white",
|
|
878
|
+
padding=(1, 2),
|
|
879
|
+
subtitle=display_server_list,
|
|
880
|
+
subtitle_align="left",
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
console.console.print(panel, markup=self._markup)
|
|
884
|
+
console.console.print("\n")
|
|
885
|
+
|
|
886
|
+
def show_parallel_results(self, parallel_agent) -> None:
|
|
887
|
+
"""Display parallel agent results in a clean, organized format.
|
|
888
|
+
|
|
889
|
+
Args:
|
|
890
|
+
parallel_agent: The parallel agent containing fan_out_agents with results
|
|
891
|
+
"""
|
|
892
|
+
|
|
893
|
+
from rich.text import Text
|
|
894
|
+
|
|
895
|
+
if self.config and not self.config.logger.show_chat:
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
if not parallel_agent or not hasattr(parallel_agent, "fan_out_agents"):
|
|
899
|
+
return
|
|
900
|
+
|
|
901
|
+
# Collect results and agent information
|
|
902
|
+
agent_results = []
|
|
903
|
+
|
|
904
|
+
for agent in parallel_agent.fan_out_agents:
|
|
905
|
+
# Get the last response text from this agent
|
|
906
|
+
message_history = agent.message_history
|
|
907
|
+
if not message_history:
|
|
908
|
+
continue
|
|
909
|
+
|
|
910
|
+
last_message = message_history[-1]
|
|
911
|
+
content = last_message.last_text()
|
|
912
|
+
|
|
913
|
+
# Get model name
|
|
914
|
+
model = "unknown"
|
|
915
|
+
if (
|
|
916
|
+
hasattr(agent, "_llm")
|
|
917
|
+
and agent._llm
|
|
918
|
+
and hasattr(agent._llm, "default_request_params")
|
|
919
|
+
):
|
|
920
|
+
model = getattr(agent._llm.default_request_params, "model", "unknown")
|
|
921
|
+
|
|
922
|
+
# Get usage information
|
|
923
|
+
tokens = 0
|
|
924
|
+
tool_calls = 0
|
|
925
|
+
if hasattr(agent, "usage_accumulator") and agent.usage_accumulator:
|
|
926
|
+
summary = agent.usage_accumulator.get_summary()
|
|
927
|
+
tokens = summary.get("cumulative_input_tokens", 0) + summary.get(
|
|
928
|
+
"cumulative_output_tokens", 0
|
|
929
|
+
)
|
|
930
|
+
tool_calls = summary.get("cumulative_tool_calls", 0)
|
|
931
|
+
|
|
932
|
+
agent_results.append(
|
|
933
|
+
{
|
|
934
|
+
"name": agent.name,
|
|
935
|
+
"model": model,
|
|
936
|
+
"content": content,
|
|
937
|
+
"tokens": tokens,
|
|
938
|
+
"tool_calls": tool_calls,
|
|
939
|
+
}
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
if not agent_results:
|
|
943
|
+
return
|
|
944
|
+
|
|
945
|
+
# Display header
|
|
946
|
+
console.console.print()
|
|
947
|
+
console.console.print("[dim]Parallel execution complete[/dim]")
|
|
948
|
+
console.console.print()
|
|
949
|
+
|
|
950
|
+
# Display results for each agent
|
|
951
|
+
for i, result in enumerate(agent_results):
|
|
952
|
+
if i > 0:
|
|
953
|
+
# Simple full-width separator
|
|
954
|
+
console.console.print()
|
|
955
|
+
console.console.print("─" * console.console.size.width, style="dim")
|
|
956
|
+
console.console.print()
|
|
957
|
+
|
|
958
|
+
# Two column header: model name (green) + usage info (dim)
|
|
959
|
+
left = f"[green]▎[/green] [bold green]{result['model']}[/bold green]"
|
|
960
|
+
|
|
961
|
+
# Build right side with tokens and tool calls if available
|
|
962
|
+
right_parts = []
|
|
963
|
+
if result["tokens"] > 0:
|
|
964
|
+
right_parts.append(f"{result['tokens']:,} tokens")
|
|
965
|
+
if result["tool_calls"] > 0:
|
|
966
|
+
right_parts.append(f"{result['tool_calls']} tools")
|
|
967
|
+
|
|
968
|
+
right = f"[dim]{' • '.join(right_parts) if right_parts else 'no usage data'}[/dim]"
|
|
969
|
+
|
|
970
|
+
# Calculate padding to right-align usage info
|
|
971
|
+
width = console.console.size.width
|
|
972
|
+
left_text = Text.from_markup(left)
|
|
973
|
+
right_text = Text.from_markup(right)
|
|
974
|
+
padding = max(1, width - left_text.cell_len - right_text.cell_len)
|
|
975
|
+
|
|
976
|
+
console.console.print(left + " " * padding + right, markup=self._markup)
|
|
977
|
+
console.console.print()
|
|
978
|
+
|
|
979
|
+
# Display content based on its type (check for markdown markers in parallel results)
|
|
980
|
+
content = result["content"]
|
|
981
|
+
# Use _display_content with assistant message type so content isn't dimmed
|
|
982
|
+
self._display_content(
|
|
983
|
+
content,
|
|
984
|
+
truncate=False,
|
|
985
|
+
is_error=False,
|
|
986
|
+
message_type=MessageType.ASSISTANT,
|
|
987
|
+
check_markdown_markers=True,
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
# Summary
|
|
991
|
+
console.console.print()
|
|
992
|
+
console.console.print("─" * console.console.size.width, style="dim")
|
|
993
|
+
|
|
994
|
+
total_tokens = sum(result["tokens"] for result in agent_results)
|
|
995
|
+
total_tools = sum(result["tool_calls"] for result in agent_results)
|
|
996
|
+
|
|
997
|
+
summary_parts = [f"{len(agent_results)} models"]
|
|
998
|
+
if total_tokens > 0:
|
|
999
|
+
summary_parts.append(f"{total_tokens:,} tokens")
|
|
1000
|
+
if total_tools > 0:
|
|
1001
|
+
summary_parts.append(f"{total_tools} tools")
|
|
1002
|
+
|
|
1003
|
+
summary_text = " • ".join(summary_parts)
|
|
1004
|
+
console.console.print(f"[dim]{summary_text}[/dim]")
|
|
1005
|
+
console.console.print()
|