fast-agent-mcp 0.4.7__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.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,1199 @@
1
+ """
2
+ Interactive prompt functionality for agents.
3
+
4
+ This module provides interactive command-line functionality for agents,
5
+ extracted from the original AgentApp implementation to support the new DirectAgentApp.
6
+
7
+ Usage:
8
+ prompt = InteractivePrompt()
9
+ await prompt.prompt_loop(
10
+ send_func=agent_app.send,
11
+ default_agent="default_agent",
12
+ available_agents=["agent1", "agent2"],
13
+ prompt_provider=agent_app
14
+ )
15
+ """
16
+
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Union, cast
19
+
20
+ from fast_agent.constants import CONTROL_MESSAGE_SAVE_HISTORY
21
+
22
+ if TYPE_CHECKING:
23
+ from fast_agent.core.agent_app import AgentApp
24
+
25
+ from mcp.types import Prompt, PromptMessage
26
+ from rich import print as rich_print
27
+
28
+ from fast_agent.agents.agent_types import AgentType
29
+ from fast_agent.history.history_exporter import HistoryExporter
30
+ from fast_agent.mcp.mcp_aggregator import SEP
31
+ from fast_agent.mcp.types import McpAgentProtocol
32
+ from fast_agent.types import PromptMessageExtended
33
+ from fast_agent.ui.enhanced_prompt import (
34
+ _display_agent_info_helper,
35
+ get_argument_input,
36
+ get_enhanced_input,
37
+ get_selection_input,
38
+ handle_special_commands,
39
+ show_mcp_status,
40
+ )
41
+ from fast_agent.ui.history_display import display_history_overview
42
+ from fast_agent.ui.progress_display import progress_display
43
+ from fast_agent.ui.usage_display import collect_agents_from_provider, display_usage_report
44
+
45
+ # Type alias for the send function
46
+ SendFunc = Callable[[Union[str, PromptMessage, PromptMessageExtended], str], Awaitable[str]]
47
+
48
+ # Type alias for the agent getter function
49
+ AgentGetter = Callable[[str], object | None]
50
+
51
+
52
+ class InteractivePrompt:
53
+ """
54
+ Provides interactive prompt functionality that works with any agent implementation.
55
+ This is extracted from the original AgentApp implementation to support DirectAgentApp.
56
+ """
57
+
58
+ def __init__(self, agent_types: dict[str, AgentType] | None = None) -> None:
59
+ """
60
+ Initialize the interactive prompt.
61
+
62
+ Args:
63
+ agent_types: Dictionary mapping agent names to their types for display
64
+ """
65
+ self.agent_types: dict[str, AgentType] = agent_types or {}
66
+
67
+ async def prompt_loop(
68
+ self,
69
+ send_func: SendFunc,
70
+ default_agent: str,
71
+ available_agents: list[str],
72
+ prompt_provider: "AgentApp",
73
+ default: str = "",
74
+ ) -> str:
75
+ """
76
+ Start an interactive prompt session.
77
+
78
+ Args:
79
+ send_func: Function to send messages to agents
80
+ default_agent: Name of the default agent to use
81
+ available_agents: List of available agent names
82
+ prompt_provider: AgentApp instance for accessing agents and prompts
83
+ default: Default message to use when user presses enter
84
+
85
+ Returns:
86
+ The result of the interactive session
87
+ """
88
+ agent = default_agent
89
+ if not agent:
90
+ if available_agents:
91
+ agent = available_agents[0]
92
+ else:
93
+ raise ValueError("No default agent available")
94
+
95
+ if agent not in available_agents:
96
+ raise ValueError(f"No agent named '{agent}'")
97
+
98
+ # Ensure we track available agents in a set for fast lookup
99
+ available_agents_set = set(available_agents)
100
+
101
+ result = ""
102
+ while True:
103
+ with progress_display.paused():
104
+ # Use the enhanced input method with advanced features
105
+ user_input = await get_enhanced_input(
106
+ agent_name=agent,
107
+ default=default,
108
+ show_default=(default != ""),
109
+ show_stop_hint=True,
110
+ multiline=False, # Default to single-line mode
111
+ available_agent_names=available_agents,
112
+ agent_types=self.agent_types, # Pass agent types for display
113
+ agent_provider=prompt_provider, # Pass agent provider for info display
114
+ )
115
+
116
+ # Handle special commands - pass "True" to enable agent switching
117
+ command_result = await handle_special_commands(user_input, True)
118
+
119
+ # Check if we should switch agents
120
+ if isinstance(command_result, dict):
121
+ command_dict: dict[str, Any] = command_result
122
+ if "switch_agent" in command_dict:
123
+ new_agent = command_dict["switch_agent"]
124
+ if new_agent in available_agents_set:
125
+ agent = new_agent
126
+ # Display new agent info immediately when switching
127
+ rich_print() # Add spacing
128
+ await _display_agent_info_helper(agent, prompt_provider)
129
+ continue
130
+ else:
131
+ rich_print(f"[red]Agent '{new_agent}' not found[/red]")
132
+ continue
133
+ # Keep the existing list_prompts handler for backward compatibility
134
+ elif "list_prompts" in command_dict:
135
+ # Use the prompt_provider directly
136
+ await self._list_prompts(prompt_provider, agent)
137
+ continue
138
+ elif "select_prompt" in command_dict:
139
+ # Handle prompt selection, using both list_prompts and apply_prompt
140
+ prompt_name = command_dict.get("prompt_name")
141
+ prompt_index = command_dict.get("prompt_index")
142
+
143
+ # If a specific index was provided (from /prompt <number>)
144
+ if prompt_index is not None:
145
+ # First get a list of all prompts to look up the index
146
+ all_prompts = await self._get_all_prompts(prompt_provider, agent)
147
+ if not all_prompts:
148
+ rich_print("[yellow]No prompts available[/yellow]")
149
+ continue
150
+
151
+ # Check if the index is valid
152
+ if 1 <= prompt_index <= len(all_prompts):
153
+ # Get the prompt at the specified index (1-based to 0-based)
154
+ selected_prompt = all_prompts[prompt_index - 1]
155
+ # Use the already created namespaced_name to ensure consistency
156
+ await self._select_prompt(
157
+ prompt_provider,
158
+ agent,
159
+ selected_prompt["namespaced_name"],
160
+ )
161
+ else:
162
+ rich_print(
163
+ f"[red]Invalid prompt number: {prompt_index}. Valid range is 1-{len(all_prompts)}[/red]"
164
+ )
165
+ # Show the prompt list for convenience
166
+ await self._list_prompts(prompt_provider, agent)
167
+ else:
168
+ # Use the name-based selection
169
+ await self._select_prompt(prompt_provider, agent, prompt_name)
170
+ continue
171
+ elif "list_tools" in command_dict:
172
+ # Handle tools list display
173
+ await self._list_tools(prompt_provider, agent)
174
+ continue
175
+ elif "list_skills" in command_dict:
176
+ await self._list_skills(prompt_provider, agent)
177
+ continue
178
+ elif "show_usage" in command_dict:
179
+ # Handle usage display
180
+ await self._show_usage(prompt_provider, agent)
181
+ continue
182
+ elif "show_history" in command_dict:
183
+ history_info = command_dict.get("show_history")
184
+ history_agent = (
185
+ history_info.get("agent") if isinstance(history_info, dict) else None
186
+ )
187
+ target_agent = history_agent or agent
188
+ try:
189
+ agent_obj = prompt_provider._agent(target_agent)
190
+ except Exception:
191
+ rich_print(f"[red]Unable to load agent '{target_agent}'[/red]")
192
+ continue
193
+
194
+ history = getattr(agent_obj, "message_history", [])
195
+ usage = getattr(agent_obj, "usage_accumulator", None)
196
+ display_history_overview(target_agent, history, usage)
197
+ continue
198
+ elif "clear_last" in command_dict:
199
+ clear_info = command_dict.get("clear_last")
200
+ clear_agent = (
201
+ clear_info.get("agent") if isinstance(clear_info, dict) else None
202
+ )
203
+ target_agent = clear_agent or agent
204
+ try:
205
+ agent_obj = prompt_provider._agent(target_agent)
206
+ except Exception:
207
+ rich_print(f"[red]Unable to load agent '{target_agent}'[/red]")
208
+ continue
209
+
210
+ removed_message = None
211
+ pop_callable = getattr(agent_obj, "pop_last_message", None)
212
+ if callable(pop_callable):
213
+ removed_message = pop_callable()
214
+ else:
215
+ history = getattr(agent_obj, "message_history", [])
216
+ if history:
217
+ try:
218
+ removed_message = history.pop()
219
+ except Exception:
220
+ removed_message = None
221
+
222
+ if removed_message:
223
+ role = getattr(removed_message, "role", "message")
224
+ role_display = role.capitalize() if isinstance(role, str) else "Message"
225
+ rich_print(
226
+ f"[green]Removed last {role_display} for agent '{target_agent}'.[/green]"
227
+ )
228
+ else:
229
+ rich_print(
230
+ f"[yellow]No messages to remove for agent '{target_agent}'.[/yellow]"
231
+ )
232
+ continue
233
+ elif "clear_history" in command_dict:
234
+ clear_info = command_dict.get("clear_history")
235
+ clear_agent = (
236
+ clear_info.get("agent") if isinstance(clear_info, dict) else None
237
+ )
238
+ target_agent = clear_agent or agent
239
+ try:
240
+ agent_obj = prompt_provider._agent(target_agent)
241
+ except Exception:
242
+ rich_print(f"[red]Unable to load agent '{target_agent}'[/red]")
243
+ continue
244
+
245
+ if hasattr(agent_obj, "clear"):
246
+ try:
247
+ agent_obj.clear()
248
+ rich_print(
249
+ f"[green]History cleared for agent '{target_agent}'.[/green]"
250
+ )
251
+ except Exception as exc:
252
+ rich_print(
253
+ f"[red]Failed to clear history for '{target_agent}': {exc}[/red]"
254
+ )
255
+ else:
256
+ rich_print(
257
+ f"[yellow]Agent '{target_agent}' does not support clearing history.[/yellow]"
258
+ )
259
+ continue
260
+ elif "show_system" in command_dict:
261
+ # Handle system prompt display
262
+ await self._show_system(prompt_provider, agent)
263
+ continue
264
+ elif "show_markdown" in command_dict:
265
+ # Handle markdown display
266
+ await self._show_markdown(prompt_provider, agent)
267
+ continue
268
+ elif "show_mcp_status" in command_dict:
269
+ rich_print()
270
+ await show_mcp_status(agent, prompt_provider)
271
+ continue
272
+ elif "save_history" in command_dict:
273
+ # Save history for the current agent
274
+ filename = command_dict.get("filename")
275
+ try:
276
+ agent_obj = prompt_provider._agent(agent)
277
+
278
+ # Prefer type-safe exporter over magic string
279
+ saved_path = await HistoryExporter.save(agent_obj, filename)
280
+ rich_print(f"[green]History saved to {saved_path}[/green]")
281
+ except Exception:
282
+ # Fallback to magic string path for maximum compatibility
283
+ control = CONTROL_MESSAGE_SAVE_HISTORY + (
284
+ f" {filename}" if filename else ""
285
+ )
286
+ result = await send_func(control, agent)
287
+ if result:
288
+ rich_print(f"[green]{result}[/green]")
289
+ continue
290
+ elif "load_history" in command_dict:
291
+ # Load history for the current agent
292
+ if command_dict.get("error"):
293
+ rich_print(f"[red]{command_dict['error']}[/red]")
294
+ continue
295
+
296
+ filename = command_dict.get("filename")
297
+ try:
298
+ from fast_agent.mcp.prompts.prompt_load import load_history_into_agent
299
+
300
+ # Get the agent object and its underlying LLM
301
+ agent_obj = prompt_provider._agent(agent)
302
+
303
+ # Load history directly without triggering LLM call
304
+ load_history_into_agent(agent_obj, Path(filename))
305
+
306
+ msg_count = len(agent_obj.message_history)
307
+ rich_print(
308
+ f"[green]Loaded {msg_count} messages from {filename}[/green]"
309
+ )
310
+ except FileNotFoundError:
311
+ rich_print(f"[red]File not found: {filename}[/red]")
312
+ except Exception as e:
313
+ rich_print(f"[red]Error loading history: {e}[/red]")
314
+ continue
315
+
316
+ # Skip further processing if:
317
+ # 1. The command was handled (command_result is truthy)
318
+ # 2. The original input was a dictionary (special command like /prompt)
319
+ # 3. The command result itself is a dictionary (special command handling result)
320
+ # This fixes the issue where /prompt without arguments gets sent to the LLM
321
+ if (
322
+ command_result
323
+ or isinstance(user_input, dict)
324
+ or isinstance(command_result, dict)
325
+ ):
326
+ continue
327
+
328
+ if user_input.upper() == "STOP":
329
+ return result
330
+ if user_input == "":
331
+ continue
332
+
333
+ # Send the message to the agent
334
+ result = await send_func(user_input, agent)
335
+
336
+ return result
337
+
338
+ def _create_combined_separator_status(
339
+ self, left_content: str, right_info: str, console
340
+ ) -> None:
341
+ """
342
+ Create a combined separator and status line using the new visual style.
343
+
344
+ Args:
345
+ left_content: The main content (block, arrow, name) - left justified with color
346
+ right_info: Supplementary information to show in brackets - right aligned
347
+ console: Rich console instance to use
348
+ """
349
+ from rich.text import Text
350
+
351
+ width = console.size.width
352
+
353
+ # Create left text
354
+ left_text = Text.from_markup(left_content)
355
+
356
+ # Create right text if we have info
357
+ if right_info and right_info.strip():
358
+ # Add dim brackets around the right info
359
+ right_text = Text()
360
+ right_text.append("[", style="dim")
361
+ right_text.append_text(Text.from_markup(right_info))
362
+ right_text.append("]", style="dim")
363
+ # Calculate separator count
364
+ separator_count = width - left_text.cell_len - right_text.cell_len
365
+ if separator_count < 1:
366
+ separator_count = 1 # Always at least 1 separator
367
+ else:
368
+ right_text = Text("")
369
+ separator_count = width - left_text.cell_len
370
+
371
+ # Build the combined line
372
+ combined = Text()
373
+ combined.append_text(left_text)
374
+ combined.append(" ", style="default")
375
+ combined.append("─" * (separator_count - 1), style="dim")
376
+ combined.append_text(right_text)
377
+
378
+ # Print with empty line before
379
+ rich_print()
380
+ console.print(combined)
381
+ rich_print()
382
+
383
+ async def _get_all_prompts(self, prompt_provider: "AgentApp", agent_name: str | None = None):
384
+ """
385
+ Get a list of all available prompts.
386
+
387
+ Args:
388
+ prompt_provider: Provider that implements list_prompts
389
+ agent_name: Optional agent name (for multi-agent apps)
390
+
391
+ Returns:
392
+ List of prompt info dictionaries, sorted by server and name
393
+ """
394
+ try:
395
+ # Call list_prompts on the provider
396
+ prompt_servers = await prompt_provider.list_prompts(
397
+ namespace=None, agent_name=agent_name
398
+ )
399
+
400
+ all_prompts = []
401
+
402
+ # Process the returned prompt servers
403
+ if prompt_servers:
404
+ # First collect all prompts
405
+ for server_name, prompts_info in prompt_servers.items():
406
+ if prompts_info and hasattr(prompts_info, "prompts") and prompts_info.prompts:
407
+ for prompt in prompts_info.prompts:
408
+ # Use the SEP constant for proper namespacing
409
+ all_prompts.append(
410
+ {
411
+ "server": server_name,
412
+ "name": prompt.name,
413
+ "namespaced_name": f"{server_name}{SEP}{prompt.name}",
414
+ "title": prompt.title or None,
415
+ "description": prompt.description or "No description",
416
+ "arg_count": len(prompt.arguments or []),
417
+ "arguments": prompt.arguments or [],
418
+ }
419
+ )
420
+ elif isinstance(prompts_info, list) and prompts_info:
421
+ for prompt in prompts_info:
422
+ if isinstance(prompt, dict) and "name" in prompt:
423
+ all_prompts.append(
424
+ {
425
+ "server": server_name,
426
+ "name": prompt["name"],
427
+ "namespaced_name": f"{server_name}{SEP}{prompt['name']}",
428
+ "title": prompt.get("title", None),
429
+ "description": prompt.get("description", "No description"),
430
+ "arg_count": len(prompt.get("arguments", [])),
431
+ "arguments": prompt.get("arguments", []),
432
+ }
433
+ )
434
+ else:
435
+ # Handle Prompt objects from mcp.types
436
+ prompt_obj = cast("Prompt", prompt)
437
+ all_prompts.append(
438
+ {
439
+ "server": server_name,
440
+ "name": prompt_obj.name,
441
+ "namespaced_name": f"{server_name}{SEP}{prompt_obj.name}",
442
+ "title": prompt_obj.title or None,
443
+ "description": prompt_obj.description or "No description",
444
+ "arg_count": len(prompt_obj.arguments or []),
445
+ "arguments": prompt_obj.arguments or [],
446
+ }
447
+ )
448
+
449
+ # Sort prompts by server and name for consistent ordering
450
+ all_prompts.sort(key=lambda p: (p["server"], p["name"]))
451
+
452
+ return all_prompts
453
+
454
+ except Exception as e:
455
+ import traceback
456
+
457
+ from rich import print as rich_print
458
+
459
+ rich_print(f"[red]Error getting prompts: {e}[/red]")
460
+ rich_print(f"[dim]{traceback.format_exc()}[/dim]")
461
+ return []
462
+
463
+ async def _list_prompts(self, prompt_provider: "AgentApp", agent_name: str) -> None:
464
+ """
465
+ List available prompts for an agent.
466
+
467
+ Args:
468
+ prompt_provider: Provider that implements list_prompts
469
+ agent_name: Name of the agent
470
+ """
471
+ try:
472
+ # Get all prompts using the helper function
473
+ all_prompts = await self._get_all_prompts(prompt_provider, agent_name)
474
+
475
+ rich_print(f"\n[bold]Prompts for agent [cyan]{agent_name}[/cyan]:[/bold]")
476
+
477
+ if not all_prompts:
478
+ rich_print("[yellow]No prompts available for this agent[/yellow]")
479
+ return
480
+
481
+ rich_print()
482
+
483
+ # Display prompts using clean compact format
484
+ for i, prompt in enumerate(all_prompts, 1):
485
+ # Main line: [ 1] server•prompt_name Title
486
+ from rich.text import Text
487
+
488
+ prompt_line = Text()
489
+ prompt_line.append(f"[{i:2}] ", style="dim cyan")
490
+ prompt_line.append(f"{prompt['server']}•", style="dim green")
491
+ prompt_line.append(prompt["name"], style="bright_blue bold")
492
+
493
+ # Add title if available
494
+ if prompt["title"] and prompt["title"].strip():
495
+ prompt_line.append(f" {prompt['title']}", style="default")
496
+
497
+ rich_print(prompt_line)
498
+
499
+ # Description lines - show 2-3 rows if needed
500
+ if prompt["description"] and prompt["description"].strip():
501
+ description = prompt["description"].strip()
502
+ # Calculate rough character limit for 2-3 lines (assuming ~80 chars per line with indent)
503
+ char_limit = 240 # About 3 lines worth
504
+
505
+ if len(description) > char_limit:
506
+ # Find a good break point near the limit (prefer sentence/word boundaries)
507
+ truncate_pos = char_limit
508
+ # Look back for sentence end
509
+ sentence_break = description.rfind(". ", 0, char_limit + 20)
510
+ if sentence_break > char_limit - 50: # If we found a nearby sentence break
511
+ truncate_pos = sentence_break + 1
512
+ else:
513
+ # Look for word boundary
514
+ word_break = description.rfind(" ", 0, char_limit + 10)
515
+ if word_break > char_limit - 30: # If we found a nearby word break
516
+ truncate_pos = word_break
517
+
518
+ description = description[:truncate_pos].rstrip() + "..."
519
+
520
+ # Split into lines and wrap
521
+ import textwrap
522
+
523
+ wrapped_lines = textwrap.wrap(description, width=72, subsequent_indent=" ")
524
+ for line in wrapped_lines:
525
+ if line.startswith(" "): # Already indented continuation line
526
+ rich_print(f" [white]{line[5:]}[/white]")
527
+ else: # First line needs indent
528
+ rich_print(f" [white]{line}[/white]")
529
+
530
+ # Arguments line - show argument names if available
531
+ if prompt["arg_count"] > 0:
532
+ arg_names = prompt.get("arg_names", [])
533
+ required_args = prompt.get("required_args", [])
534
+
535
+ if arg_names:
536
+ arg_list = []
537
+ for arg_name in arg_names:
538
+ if arg_name in required_args:
539
+ arg_list.append(f"{arg_name}*")
540
+ else:
541
+ arg_list.append(arg_name)
542
+
543
+ args_text = ", ".join(arg_list)
544
+ if len(args_text) > 80:
545
+ args_text = args_text[:77] + "..."
546
+ rich_print(f" [dim magenta]args: {args_text}[/dim magenta]")
547
+ else:
548
+ rich_print(
549
+ f" [dim magenta]args: {prompt['arg_count']} parameter{'s' if prompt['arg_count'] != 1 else ''}[/dim magenta]"
550
+ )
551
+
552
+ rich_print() # Space between prompts
553
+
554
+ # Add usage instructions
555
+ rich_print(
556
+ "[dim]Usage: /prompt <number> to select by number, or /prompts for interactive selection[/dim]"
557
+ )
558
+
559
+ except Exception as e:
560
+ import traceback
561
+
562
+ rich_print(f"[red]Error listing prompts: {e}[/red]")
563
+ rich_print(f"[dim]{traceback.format_exc()}[/dim]")
564
+
565
+ async def _select_prompt(
566
+ self,
567
+ prompt_provider: "AgentApp",
568
+ agent_name: str,
569
+ requested_name: str | None = None,
570
+ send_func: SendFunc | None = None,
571
+ ) -> None:
572
+ """
573
+ Select and apply a prompt.
574
+
575
+ Args:
576
+ prompt_provider: Provider that implements list_prompts and get_prompt
577
+ agent_name: Name of the agent
578
+ requested_name: Optional name of the prompt to apply
579
+ """
580
+ try:
581
+ # Get all available prompts directly from the prompt provider
582
+ rich_print(f"\n[bold]Fetching prompts for agent [cyan]{agent_name}[/cyan]...[/bold]")
583
+
584
+ # Call list_prompts on the provider
585
+ prompt_servers = await prompt_provider.list_prompts(
586
+ namespace=None, agent_name=agent_name
587
+ )
588
+
589
+ if not prompt_servers:
590
+ rich_print("[yellow]No prompts available for this agent[/yellow]")
591
+ return
592
+
593
+ # Process fetched prompts
594
+ all_prompts = []
595
+ for server_name, prompts_info in prompt_servers.items():
596
+ if not prompts_info:
597
+ continue
598
+
599
+ # Extract prompts
600
+ prompts: list[Prompt] = []
601
+ if hasattr(prompts_info, "prompts"):
602
+ prompts = prompts_info.prompts
603
+ elif isinstance(prompts_info, list):
604
+ prompts = prompts_info
605
+
606
+ # Process each prompt
607
+ for prompt in prompts:
608
+ # Get basic prompt info
609
+ prompt_name = prompt.name
610
+ prompt_title = prompt.title or None
611
+ prompt_description = prompt.description or "No description"
612
+
613
+ # Extract argument information
614
+ arg_names = []
615
+ required_args = []
616
+ optional_args = []
617
+ arg_descriptions = {}
618
+
619
+ # Get arguments list
620
+ if prompt.arguments:
621
+ for arg in prompt.arguments:
622
+ arg_names.append(arg.name)
623
+
624
+ # Store description if available
625
+ if arg.description:
626
+ arg_descriptions[arg.name] = arg.description
627
+
628
+ # Check if required
629
+ if arg.required:
630
+ required_args.append(arg.name)
631
+ else:
632
+ optional_args.append(arg.name)
633
+
634
+ # Create namespaced version using the consistent separator
635
+ namespaced_name = f"{server_name}{SEP}{prompt_name}"
636
+
637
+ # Add to collection
638
+ all_prompts.append(
639
+ {
640
+ "server": server_name,
641
+ "name": prompt_name,
642
+ "namespaced_name": namespaced_name,
643
+ "title": prompt_title,
644
+ "description": prompt_description,
645
+ "arg_count": len(arg_names),
646
+ "arg_names": arg_names,
647
+ "required_args": required_args,
648
+ "optional_args": optional_args,
649
+ "arg_descriptions": arg_descriptions,
650
+ }
651
+ )
652
+
653
+ if not all_prompts:
654
+ rich_print("[yellow]No prompts available for this agent[/yellow]")
655
+ return
656
+
657
+ # Sort prompts by server then name
658
+ all_prompts.sort(key=lambda p: (p["server"], p["name"]))
659
+
660
+ # Handle specifically requested prompt
661
+ if requested_name:
662
+ matching_prompts = [
663
+ p
664
+ for p in all_prompts
665
+ if p["name"] == requested_name or p["namespaced_name"] == requested_name
666
+ ]
667
+
668
+ if not matching_prompts:
669
+ rich_print(f"[red]Prompt '{requested_name}' not found[/red]")
670
+ rich_print("[yellow]Available prompts:[/yellow]")
671
+ for p in all_prompts:
672
+ rich_print(f" {p['namespaced_name']}")
673
+ return
674
+
675
+ # If exactly one match, use it
676
+ if len(matching_prompts) == 1:
677
+ selected_prompt = matching_prompts[0]
678
+ else:
679
+ # Handle multiple matches
680
+ rich_print(f"[yellow]Multiple prompts match '{requested_name}':[/yellow]")
681
+ for i, p in enumerate(matching_prompts):
682
+ rich_print(f" {i + 1}. {p['namespaced_name']} - {p['description']}")
683
+
684
+ # Get user selection
685
+ selection = (
686
+ await get_selection_input("Enter prompt number to select: ", default="1")
687
+ or ""
688
+ )
689
+
690
+ try:
691
+ idx = int(selection) - 1
692
+ if 0 <= idx < len(matching_prompts):
693
+ selected_prompt = matching_prompts[idx]
694
+ else:
695
+ rich_print("[red]Invalid selection[/red]")
696
+ return
697
+ except ValueError:
698
+ rich_print("[red]Invalid input, please enter a number[/red]")
699
+ return
700
+ else:
701
+ # Show prompt selection UI using clean compact format
702
+ rich_print(f"\n[bold]Select a prompt for agent [cyan]{agent_name}[/cyan]:[/bold]")
703
+ rich_print()
704
+
705
+ # Display prompts using the same format as _list_prompts
706
+ for i, prompt in enumerate(all_prompts, 1):
707
+ # Main line: [ 1] server•prompt_name Title
708
+ from rich.text import Text
709
+
710
+ prompt_line = Text()
711
+ prompt_line.append(f"[{i:2}] ", style="dim cyan")
712
+ prompt_line.append(f"{prompt['server']}•", style="dim green")
713
+ prompt_line.append(prompt["name"], style="bright_blue bold")
714
+
715
+ # Add title if available
716
+ if prompt["title"] and prompt["title"].strip():
717
+ prompt_line.append(f" {prompt['title']}", style="default")
718
+
719
+ rich_print(prompt_line)
720
+
721
+ # Description lines - show 2-3 rows if needed
722
+ if prompt["description"] and prompt["description"].strip():
723
+ description = prompt["description"].strip()
724
+ # Calculate rough character limit for 2-3 lines (assuming ~80 chars per line with indent)
725
+ char_limit = 240 # About 3 lines worth
726
+
727
+ if len(description) > char_limit:
728
+ # Find a good break point near the limit (prefer sentence/word boundaries)
729
+ truncate_pos = char_limit
730
+ # Look back for sentence end
731
+ sentence_break = description.rfind(". ", 0, char_limit + 20)
732
+ if (
733
+ sentence_break > char_limit - 50
734
+ ): # If we found a nearby sentence break
735
+ truncate_pos = sentence_break + 1
736
+ else:
737
+ # Look for word boundary
738
+ word_break = description.rfind(" ", 0, char_limit + 10)
739
+ if word_break > char_limit - 30: # If we found a nearby word break
740
+ truncate_pos = word_break
741
+
742
+ description = description[:truncate_pos].rstrip() + "..."
743
+
744
+ # Split into lines and wrap
745
+ import textwrap
746
+
747
+ wrapped_lines = textwrap.wrap(
748
+ description, width=72, subsequent_indent=" "
749
+ )
750
+ for line in wrapped_lines:
751
+ if line.startswith(" "): # Already indented continuation line
752
+ rich_print(f" [white]{line[5:]}[/white]")
753
+ else: # First line needs indent
754
+ rich_print(f" [white]{line}[/white]")
755
+
756
+ # Arguments line - show argument names if available
757
+ if prompt["arg_count"] > 0:
758
+ arg_names = prompt.get("arg_names", [])
759
+ required_args = prompt.get("required_args", [])
760
+
761
+ if arg_names:
762
+ arg_list = []
763
+ for arg_name in arg_names:
764
+ if arg_name in required_args:
765
+ arg_list.append(f"{arg_name}*")
766
+ else:
767
+ arg_list.append(arg_name)
768
+
769
+ args_text = ", ".join(arg_list)
770
+ if len(args_text) > 80:
771
+ args_text = args_text[:77] + "..."
772
+ rich_print(f" [dim magenta]args: {args_text}[/dim magenta]")
773
+ else:
774
+ rich_print(
775
+ f" [dim magenta]args: {prompt['arg_count']} parameter{'s' if prompt['arg_count'] != 1 else ''}[/dim magenta]"
776
+ )
777
+
778
+ rich_print() # Space between prompts
779
+
780
+ prompt_names = [str(i) for i, _ in enumerate(all_prompts, 1)]
781
+
782
+ # Get user selection
783
+ selection = await get_selection_input(
784
+ "Enter prompt number to select (or press Enter to cancel): ",
785
+ options=prompt_names,
786
+ allow_cancel=True,
787
+ )
788
+
789
+ # Handle cancellation
790
+ if not selection or selection.strip() == "":
791
+ rich_print("[yellow]Prompt selection cancelled[/yellow]")
792
+ return
793
+
794
+ try:
795
+ idx = int(selection) - 1
796
+ if 0 <= idx < len(all_prompts):
797
+ selected_prompt = all_prompts[idx]
798
+ else:
799
+ rich_print("[red]Invalid selection[/red]")
800
+ return
801
+ except ValueError:
802
+ rich_print("[red]Invalid input, please enter a number[/red]")
803
+ return
804
+
805
+ # Get prompt arguments
806
+ required_args = selected_prompt["required_args"]
807
+ optional_args = selected_prompt["optional_args"]
808
+ arg_descriptions = selected_prompt.get("arg_descriptions", {})
809
+ arg_values = {}
810
+
811
+ # Show argument info if any
812
+ if required_args or optional_args:
813
+ if required_args and optional_args:
814
+ rich_print(
815
+ f"\n[bold]Prompt [cyan]{selected_prompt['name']}[/cyan] requires {len(required_args)} arguments and has {len(optional_args)} optional arguments:[/bold]"
816
+ )
817
+ elif required_args:
818
+ rich_print(
819
+ f"\n[bold]Prompt [cyan]{selected_prompt['name']}[/cyan] requires {len(required_args)} arguments:[/bold]"
820
+ )
821
+ elif optional_args:
822
+ rich_print(
823
+ f"\n[bold]Prompt [cyan]{selected_prompt['name']}[/cyan] has {len(optional_args)} optional arguments:[/bold]"
824
+ )
825
+
826
+ # Collect required arguments
827
+ for arg_name in required_args:
828
+ description = arg_descriptions.get(arg_name, "")
829
+ arg_value = await get_argument_input(
830
+ arg_name=arg_name,
831
+ description=description,
832
+ required=True,
833
+ )
834
+ if arg_value is not None:
835
+ arg_values[arg_name] = arg_value
836
+
837
+ # Collect optional arguments
838
+ if optional_args:
839
+ for arg_name in optional_args:
840
+ description = arg_descriptions.get(arg_name, "")
841
+ arg_value = await get_argument_input(
842
+ arg_name=arg_name,
843
+ description=description,
844
+ required=False,
845
+ )
846
+ if arg_value:
847
+ arg_values[arg_name] = arg_value
848
+
849
+ # Apply the prompt using generate() for proper progress display
850
+ namespaced_name = selected_prompt["namespaced_name"]
851
+ rich_print(f"\n[bold]Applying prompt [cyan]{namespaced_name}[/cyan]...[/bold]")
852
+
853
+ # Get the agent directly for generate() call
854
+ assert hasattr(prompt_provider, "_agent"), (
855
+ "Interactive prompt expects an AgentApp with _agent()"
856
+ )
857
+ agent = prompt_provider._agent(agent_name)
858
+
859
+ try:
860
+ # Use agent.apply_prompt() which handles everything properly:
861
+ # - get_prompt() to fetch template
862
+ # - convert to multipart
863
+ # - call generate() for progress display
864
+ # - return response text
865
+ # Response display is handled by the agent's show_ methods, don't print it here
866
+
867
+ # Fetch the prompt first (without progress display)
868
+ prompt_result = await agent.get_prompt(namespaced_name, arg_values)
869
+
870
+ if not prompt_result or not prompt_result.messages:
871
+ rich_print(
872
+ f"[red]Prompt '{namespaced_name}' could not be found or contains no messages[/red]"
873
+ )
874
+ return
875
+
876
+ # Convert to multipart format
877
+ from fast_agent.types import PromptMessageExtended
878
+
879
+ multipart_messages = PromptMessageExtended.from_get_prompt_result(prompt_result)
880
+
881
+ # Now start progress display for the actual generation
882
+ progress_display.resume()
883
+ try:
884
+ await agent.generate(multipart_messages, None)
885
+ finally:
886
+ # Pause again for the next UI interaction
887
+ progress_display.pause()
888
+
889
+ # Show usage info after the turn (same as send_wrapper does)
890
+ prompt_provider._show_turn_usage(agent_name)
891
+
892
+ except Exception as e:
893
+ rich_print(f"[red]Error applying prompt: {e}[/red]")
894
+
895
+ except Exception as e:
896
+ import traceback
897
+
898
+ rich_print(f"[red]Error selecting or applying prompt: {e}[/red]")
899
+ rich_print(f"[dim]{traceback.format_exc()}[/dim]")
900
+
901
+ async def _list_tools(self, prompt_provider: "AgentApp", agent_name: str) -> None:
902
+ """
903
+ List available tools for an agent.
904
+
905
+ Args:
906
+ prompt_provider: Provider that implements list_tools
907
+ agent_name: Name of the agent
908
+ """
909
+ try:
910
+ # Get agent to list tools from
911
+ assert hasattr(prompt_provider, "_agent"), (
912
+ "Interactive prompt expects an AgentApp with _agent()"
913
+ )
914
+ agent = prompt_provider._agent(agent_name)
915
+
916
+ rich_print(f"\n[bold]Tools for agent [cyan]{agent_name}[/cyan]:[/bold]")
917
+
918
+ # Get tools using list_tools
919
+ tools_result = await agent.list_tools()
920
+
921
+ if not tools_result or not hasattr(tools_result, "tools") or not tools_result.tools:
922
+ rich_print("[yellow]No tools available for this agent[/yellow]")
923
+ return
924
+
925
+ rich_print()
926
+
927
+ # Display tools using clean compact format
928
+ index = 1
929
+ for tool in tools_result.tools:
930
+ # Main line: [ 1] tool_name Title
931
+ from rich.text import Text
932
+
933
+ meta = getattr(tool, "meta", {}) or {}
934
+
935
+ tool_line = Text()
936
+ tool_line.append(f"[{index:2}] ", style="dim cyan")
937
+ tool_line.append(tool.name, style="bright_blue bold")
938
+
939
+ # Add title if available
940
+ if tool.title and tool.title.strip():
941
+ tool_line.append(f" {tool.title}", style="default")
942
+
943
+ if meta.get("openai/skybridgeEnabled"):
944
+ tool_line.append(" (skybridge)", style="cyan")
945
+
946
+ rich_print(tool_line)
947
+
948
+ # Description lines - show 2-3 rows if needed
949
+ if tool.description and tool.description.strip():
950
+ description = tool.description.strip()
951
+ # Calculate rough character limit for 2-3 lines (assuming ~80 chars per line with indent)
952
+ char_limit = 240 # About 3 lines worth
953
+
954
+ if len(description) > char_limit:
955
+ # Find a good break point near the limit (prefer sentence/word boundaries)
956
+ truncate_pos = char_limit
957
+ # Look back for sentence end
958
+ sentence_break = description.rfind(". ", 0, char_limit + 20)
959
+ if sentence_break > char_limit - 50: # If we found a nearby sentence break
960
+ truncate_pos = sentence_break + 1
961
+ else:
962
+ # Look for word boundary
963
+ word_break = description.rfind(" ", 0, char_limit + 10)
964
+ if word_break > char_limit - 30: # If we found a nearby word break
965
+ truncate_pos = word_break
966
+
967
+ description = description[:truncate_pos].rstrip() + "..."
968
+
969
+ # Split into lines and wrap
970
+ import textwrap
971
+
972
+ wrapped_lines = textwrap.wrap(description, width=72, subsequent_indent=" ")
973
+ for line in wrapped_lines:
974
+ if line.startswith(" "): # Already indented continuation line
975
+ rich_print(f" [white]{line[5:]}[/white]")
976
+ else: # First line needs indent
977
+ rich_print(f" [white]{line}[/white]")
978
+
979
+ # Arguments line - show schema info if available
980
+ if hasattr(tool, "inputSchema") and tool.inputSchema:
981
+ schema = tool.inputSchema
982
+ if "properties" in schema:
983
+ properties = schema["properties"]
984
+ required = schema.get("required", [])
985
+
986
+ arg_list = []
987
+ for prop_name, prop_info in properties.items():
988
+ if prop_name in required:
989
+ arg_list.append(f"{prop_name}*")
990
+ else:
991
+ arg_list.append(prop_name)
992
+
993
+ if arg_list:
994
+ args_text = ", ".join(arg_list)
995
+ if len(args_text) > 80:
996
+ args_text = args_text[:77] + "..."
997
+ rich_print(f" [dim magenta]args: {args_text}[/dim magenta]")
998
+
999
+ if meta.get("openai/skybridgeEnabled"):
1000
+ template = meta.get("openai/skybridgeTemplate")
1001
+ if template:
1002
+ rich_print(f" [dim magenta]template:[/dim magenta] {template}")
1003
+
1004
+ rich_print() # Space between tools
1005
+ index += 1
1006
+
1007
+ if index == 1:
1008
+ rich_print("[yellow]No MCP tools available for this agent[/yellow]")
1009
+ except Exception as e:
1010
+ import traceback
1011
+
1012
+ rich_print(f"[red]Error listing tools: {e}[/red]")
1013
+ rich_print(f"[dim]{traceback.format_exc()}[/dim]")
1014
+
1015
+ async def _list_skills(self, prompt_provider: "AgentApp", agent_name: str) -> None:
1016
+ """List available local skills for an agent."""
1017
+
1018
+ try:
1019
+ assert hasattr(prompt_provider, "_agent"), (
1020
+ "Interactive prompt expects an AgentApp with _agent()"
1021
+ )
1022
+ agent = prompt_provider._agent(agent_name)
1023
+
1024
+ rich_print(f"\n[bold]Skills for agent [cyan]{agent_name}[/cyan]:[/bold]")
1025
+
1026
+ skill_manifests = getattr(agent, "_skill_manifests", None)
1027
+ manifests = list(skill_manifests) if skill_manifests else []
1028
+
1029
+ if not manifests:
1030
+ rich_print("[yellow]No skills available for this agent[/yellow]")
1031
+ return
1032
+
1033
+ rich_print()
1034
+
1035
+ for index, manifest in enumerate(manifests, 1):
1036
+ from rich.text import Text
1037
+
1038
+ name = getattr(manifest, "name", "")
1039
+ description = getattr(manifest, "description", "")
1040
+ path = Path(getattr(manifest, "path", Path()))
1041
+
1042
+ tool_line = Text()
1043
+ tool_line.append(f"[{index:2}] ", style="dim cyan")
1044
+ tool_line.append(name, style="bright_blue bold")
1045
+ rich_print(tool_line)
1046
+
1047
+ if description:
1048
+ import textwrap
1049
+
1050
+ wrapped_lines = textwrap.wrap(
1051
+ description.strip(), width=72, subsequent_indent=" "
1052
+ )
1053
+ for line in wrapped_lines:
1054
+ if line.startswith(" "):
1055
+ rich_print(f" [white]{line[5:]}[/white]")
1056
+ else:
1057
+ rich_print(f" [white]{line}[/white]")
1058
+
1059
+ source_path = path if path else Path(".")
1060
+ if source_path.is_file():
1061
+ source_path = source_path.parent
1062
+ try:
1063
+ display_path = source_path.relative_to(Path.cwd())
1064
+ except ValueError:
1065
+ display_path = source_path
1066
+
1067
+ rich_print(f" [dim green]source:[/dim green] {display_path}")
1068
+ rich_print()
1069
+
1070
+ except Exception as exc: # noqa: BLE001
1071
+ import traceback
1072
+
1073
+ rich_print(f"[red]Error listing skills: {exc}[/red]")
1074
+ rich_print(f"[dim]{traceback.format_exc()}[/dim]")
1075
+
1076
+ async def _show_usage(self, prompt_provider: "AgentApp", agent_name: str) -> None:
1077
+ """
1078
+ Show usage statistics for the current agent(s) in a colorful table format.
1079
+
1080
+ Args:
1081
+ prompt_provider: Provider that has access to agents
1082
+ agent_name: Name of the current agent
1083
+ """
1084
+ try:
1085
+ # Collect all agents from the prompt provider
1086
+ agents_to_show = collect_agents_from_provider(prompt_provider, agent_name)
1087
+
1088
+ if not agents_to_show:
1089
+ rich_print("[yellow]No usage data available[/yellow]")
1090
+ return
1091
+
1092
+ # Use the shared display utility
1093
+ display_usage_report(agents_to_show, show_if_progress_disabled=True)
1094
+
1095
+ except Exception as e:
1096
+ rich_print(f"[red]Error showing usage: {e}[/red]")
1097
+
1098
+ async def _show_system(self, prompt_provider: "AgentApp", agent_name: str) -> None:
1099
+ """
1100
+ Show the current system prompt for the agent.
1101
+
1102
+ Args:
1103
+ prompt_provider: Provider that has access to agents
1104
+ agent_name: Name of the current agent
1105
+ """
1106
+ try:
1107
+ # Get agent to display from
1108
+ assert hasattr(prompt_provider, "_agent"), (
1109
+ "Interactive prompt expects an AgentApp with _agent()"
1110
+ )
1111
+ agent = prompt_provider._agent(agent_name)
1112
+
1113
+ # Get the system prompt
1114
+ system_prompt = getattr(agent, "instruction", None)
1115
+ if not system_prompt:
1116
+ rich_print("[yellow]No system prompt available[/yellow]")
1117
+ return
1118
+
1119
+ # Get server count for display
1120
+ server_count = 0
1121
+ if isinstance(agent, McpAgentProtocol):
1122
+ server_names = agent.aggregator.server_names
1123
+ server_count = len(server_names) if server_names else 0
1124
+
1125
+ # Use the display utility to show the system prompt
1126
+ agent_display = getattr(agent, "display", None)
1127
+ if agent_display:
1128
+ agent_display.show_system_message(
1129
+ system_prompt=system_prompt, agent_name=agent_name, server_count=server_count
1130
+ )
1131
+ else:
1132
+ # Fallback to basic display
1133
+ from fast_agent.ui.console_display import ConsoleDisplay
1134
+
1135
+ agent_context = getattr(agent, "context", None)
1136
+ display = ConsoleDisplay(
1137
+ config=agent_context.config if hasattr(agent_context, "config") else None
1138
+ )
1139
+ display.show_system_message(
1140
+ system_prompt=system_prompt, agent_name=agent_name, server_count=server_count
1141
+ )
1142
+
1143
+ except Exception as e:
1144
+ import traceback
1145
+
1146
+ rich_print(f"[red]Error showing system prompt: {e}[/red]")
1147
+ rich_print(f"[dim]{traceback.format_exc()}[/dim]")
1148
+
1149
+ async def _show_markdown(self, prompt_provider: "AgentApp", agent_name: str) -> None:
1150
+ """
1151
+ Show the last assistant message without markdown formatting.
1152
+
1153
+ Args:
1154
+ prompt_provider: Provider that has access to agents
1155
+ agent_name: Name of the current agent
1156
+ """
1157
+ try:
1158
+ # Get agent to display from
1159
+ assert hasattr(prompt_provider, "_agent"), (
1160
+ "Interactive prompt expects an AgentApp with _agent()"
1161
+ )
1162
+ agent = prompt_provider._agent(agent_name)
1163
+
1164
+ # Check if agent has message history
1165
+ if not agent.llm:
1166
+ rich_print("[yellow]No message history available[/yellow]")
1167
+ return
1168
+
1169
+ message_history = agent.llm.message_history
1170
+ if not message_history:
1171
+ rich_print("[yellow]No messages in history[/yellow]")
1172
+ return
1173
+
1174
+ # Find the last assistant message
1175
+ last_assistant_msg = None
1176
+ for msg in reversed(message_history):
1177
+ if msg.role == "assistant":
1178
+ last_assistant_msg = msg
1179
+ break
1180
+
1181
+ if not last_assistant_msg:
1182
+ rich_print("[yellow]No assistant messages found[/yellow]")
1183
+ return
1184
+
1185
+ # Get the text content and display without markdown
1186
+ content = last_assistant_msg.last_text()
1187
+
1188
+ # Display with a simple header
1189
+ rich_print("\n[bold blue]Last Assistant Response (Plain Text):[/bold blue]")
1190
+ rich_print("─" * 60)
1191
+ # Use console.print with markup=False to display raw text
1192
+ from fast_agent.ui import console
1193
+
1194
+ console.console.print(content, markup=False)
1195
+ rich_print("─" * 60)
1196
+ rich_print()
1197
+
1198
+ except Exception as e:
1199
+ rich_print(f"[red]Error showing markdown: {e}[/red]")