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.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced prompt functionality with advanced prompt_toolkit features.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
from importlib.metadata import version
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from prompt_toolkit import PromptSession
|
|
16
|
+
from prompt_toolkit.completion import Completer, Completion, WordCompleter
|
|
17
|
+
from prompt_toolkit.filters import Condition
|
|
18
|
+
from prompt_toolkit.formatted_text import HTML
|
|
19
|
+
from prompt_toolkit.history import InMemoryHistory
|
|
20
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
21
|
+
from prompt_toolkit.styles import Style
|
|
22
|
+
from rich import print as rich_print
|
|
23
|
+
|
|
24
|
+
from fast_agent.agents.agent_types import AgentType
|
|
25
|
+
from fast_agent.constants import FAST_AGENT_ERROR_CHANNEL, FAST_AGENT_REMOVED_METADATA_CHANNEL
|
|
26
|
+
from fast_agent.core.exceptions import PromptExitError
|
|
27
|
+
from fast_agent.llm.model_info import ModelInfo
|
|
28
|
+
from fast_agent.mcp.types import McpAgentProtocol
|
|
29
|
+
from fast_agent.ui.mcp_display import render_mcp_status
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from fast_agent.core.agent_app import AgentApp
|
|
33
|
+
|
|
34
|
+
# Get the application version
|
|
35
|
+
try:
|
|
36
|
+
app_version = version("fast-agent-mcp")
|
|
37
|
+
except: # noqa: E722
|
|
38
|
+
app_version = "unknown"
|
|
39
|
+
|
|
40
|
+
# Map of agent names to their history
|
|
41
|
+
agent_histories = {}
|
|
42
|
+
|
|
43
|
+
# Store available agents for auto-completion
|
|
44
|
+
available_agents = set()
|
|
45
|
+
|
|
46
|
+
# Keep track of multi-line mode state
|
|
47
|
+
in_multiline_mode = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _extract_alert_flags_from_meta(blocks) -> set[str]:
|
|
51
|
+
flags: set[str] = set()
|
|
52
|
+
for block in blocks or []:
|
|
53
|
+
text = getattr(block, "text", None)
|
|
54
|
+
if not text:
|
|
55
|
+
continue
|
|
56
|
+
try:
|
|
57
|
+
payload = json.loads(text)
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
continue
|
|
60
|
+
if payload.get("type") != "fast-agent-removed":
|
|
61
|
+
continue
|
|
62
|
+
category = payload.get("category")
|
|
63
|
+
match category:
|
|
64
|
+
case "text":
|
|
65
|
+
flags.add("T")
|
|
66
|
+
case "document":
|
|
67
|
+
flags.add("D")
|
|
68
|
+
case "vision":
|
|
69
|
+
flags.add("V")
|
|
70
|
+
return flags
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Track whether help text has been shown globally
|
|
74
|
+
help_message_shown = False
|
|
75
|
+
|
|
76
|
+
# Track which agents have shown their info
|
|
77
|
+
_agent_info_shown = set()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def show_mcp_status(agent_name: str, agent_provider: "AgentApp | None") -> None:
|
|
81
|
+
if agent_provider is None:
|
|
82
|
+
rich_print("[red]No agent provider available[/red]")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
agent = agent_provider._agent(agent_name)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
rich_print(f"[red]Unable to load agent '{agent_name}': {exc}[/red]")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
await render_mcp_status(agent)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def _display_agent_info_helper(agent_name: str, agent_provider: "AgentApp | None") -> None:
|
|
95
|
+
"""Helper function to display agent information."""
|
|
96
|
+
# Only show once per agent
|
|
97
|
+
if agent_name in _agent_info_shown:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Get agent info from AgentApp
|
|
102
|
+
if agent_provider is None:
|
|
103
|
+
return
|
|
104
|
+
agent = agent_provider._agent(agent_name)
|
|
105
|
+
|
|
106
|
+
# Get counts TODO -- add this to the type library or adjust the way aggregator/reporting works
|
|
107
|
+
server_count = 0
|
|
108
|
+
if isinstance(agent, McpAgentProtocol):
|
|
109
|
+
server_names = agent.aggregator.server_names
|
|
110
|
+
server_count = len(server_names) if server_names else 0
|
|
111
|
+
|
|
112
|
+
tools_result = await agent.list_tools()
|
|
113
|
+
tool_count = (
|
|
114
|
+
len(tools_result.tools) if tools_result and hasattr(tools_result, "tools") else 0
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
resources_dict = await agent.list_resources()
|
|
118
|
+
resource_count = (
|
|
119
|
+
sum(len(resources) for resources in resources_dict.values()) if resources_dict else 0
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
prompts_dict = await agent.list_prompts()
|
|
123
|
+
prompt_count = sum(len(prompts) for prompts in prompts_dict.values()) if prompts_dict else 0
|
|
124
|
+
|
|
125
|
+
skill_count = 0
|
|
126
|
+
skill_manifests = getattr(agent, "_skill_manifests", None)
|
|
127
|
+
if skill_manifests:
|
|
128
|
+
try:
|
|
129
|
+
skill_count = len(list(skill_manifests))
|
|
130
|
+
except TypeError:
|
|
131
|
+
skill_count = 0
|
|
132
|
+
|
|
133
|
+
# Handle different agent types
|
|
134
|
+
if agent.agent_type == AgentType.PARALLEL:
|
|
135
|
+
# Count child agents for parallel agents
|
|
136
|
+
child_count = 0
|
|
137
|
+
if hasattr(agent, "fan_out_agents") and agent.fan_out_agents:
|
|
138
|
+
child_count += len(agent.fan_out_agents)
|
|
139
|
+
if hasattr(agent, "fan_in_agent") and agent.fan_in_agent:
|
|
140
|
+
child_count += 1
|
|
141
|
+
|
|
142
|
+
if child_count > 0:
|
|
143
|
+
child_word = "child agent" if child_count == 1 else "child agents"
|
|
144
|
+
rich_print(
|
|
145
|
+
f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {child_count:,}[dim] {child_word}[/dim]"
|
|
146
|
+
)
|
|
147
|
+
elif agent.agent_type == AgentType.ROUTER:
|
|
148
|
+
# Count child agents for router agents
|
|
149
|
+
child_count = 0
|
|
150
|
+
if hasattr(agent, "routing_agents") and agent.routing_agents:
|
|
151
|
+
child_count = len(agent.routing_agents)
|
|
152
|
+
elif hasattr(agent, "agents") and agent.agents:
|
|
153
|
+
child_count = len(agent.agents)
|
|
154
|
+
|
|
155
|
+
if child_count > 0:
|
|
156
|
+
child_word = "child agent" if child_count == 1 else "child agents"
|
|
157
|
+
rich_print(
|
|
158
|
+
f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {child_count:,}[dim] {child_word}[/dim]"
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
content_parts = []
|
|
162
|
+
|
|
163
|
+
if server_count > 0:
|
|
164
|
+
sub_parts = []
|
|
165
|
+
if tool_count > 0:
|
|
166
|
+
tool_word = "tool" if tool_count == 1 else "tools"
|
|
167
|
+
sub_parts.append(f"{tool_count:,}[dim] {tool_word}[/dim]")
|
|
168
|
+
if prompt_count > 0:
|
|
169
|
+
prompt_word = "prompt" if prompt_count == 1 else "prompts"
|
|
170
|
+
sub_parts.append(f"{prompt_count:,}[dim] {prompt_word}[/dim]")
|
|
171
|
+
if resource_count > 0:
|
|
172
|
+
resource_word = "resource" if resource_count == 1 else "resources"
|
|
173
|
+
sub_parts.append(f"{resource_count:,}[dim] {resource_word}[/dim]")
|
|
174
|
+
|
|
175
|
+
server_word = "Server" if server_count == 1 else "Servers"
|
|
176
|
+
server_text = f"{server_count:,}[dim] MCP {server_word}[/dim]"
|
|
177
|
+
if sub_parts:
|
|
178
|
+
server_text = (
|
|
179
|
+
f"{server_text}[dim] ([/dim]"
|
|
180
|
+
+ "[dim], [/dim]".join(sub_parts)
|
|
181
|
+
+ "[dim])[/dim]"
|
|
182
|
+
)
|
|
183
|
+
content_parts.append(server_text)
|
|
184
|
+
|
|
185
|
+
if skill_count > 0:
|
|
186
|
+
skill_word = "skill" if skill_count == 1 else "skills"
|
|
187
|
+
content_parts.append(
|
|
188
|
+
f"{skill_count:,}[dim] {skill_word}[/dim][dim] available[/dim]"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if content_parts:
|
|
192
|
+
content = "[dim]. [/dim]".join(content_parts)
|
|
193
|
+
rich_print(f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {content}")
|
|
194
|
+
# await _render_mcp_status(agent)
|
|
195
|
+
|
|
196
|
+
# Display Skybridge status (if aggregator discovered any)
|
|
197
|
+
try:
|
|
198
|
+
aggregator = agent.aggregator if isinstance(agent, McpAgentProtocol) else None
|
|
199
|
+
display = getattr(agent, "display", None)
|
|
200
|
+
if aggregator and display and hasattr(display, "show_skybridge_summary"):
|
|
201
|
+
skybridge_configs = await aggregator.get_skybridge_configs()
|
|
202
|
+
display.show_skybridge_summary(agent_name, skybridge_configs)
|
|
203
|
+
except Exception:
|
|
204
|
+
# Ignore Skybridge rendering issues to avoid interfering with startup
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
# Mark as shown
|
|
208
|
+
_agent_info_shown.add(agent_name)
|
|
209
|
+
|
|
210
|
+
except Exception:
|
|
211
|
+
# Silently ignore errors to not disrupt the user experience
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def _display_all_agents_with_hierarchy(
|
|
216
|
+
available_agents: list[str], agent_provider: "AgentApp | None"
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Display all agents with tree structure for workflow agents."""
|
|
219
|
+
# Track which agents are children to avoid displaying them twice
|
|
220
|
+
child_agents = set()
|
|
221
|
+
|
|
222
|
+
# First pass: identify all child agents
|
|
223
|
+
for agent_name in available_agents:
|
|
224
|
+
try:
|
|
225
|
+
if agent_provider is None:
|
|
226
|
+
continue
|
|
227
|
+
agent = agent_provider._agent(agent_name)
|
|
228
|
+
|
|
229
|
+
if agent.agent_type == AgentType.PARALLEL:
|
|
230
|
+
if hasattr(agent, "fan_out_agents") and agent.fan_out_agents:
|
|
231
|
+
for child_agent in agent.fan_out_agents:
|
|
232
|
+
child_agents.add(child_agent.name)
|
|
233
|
+
if hasattr(agent, "fan_in_agent") and agent.fan_in_agent:
|
|
234
|
+
child_agents.add(agent.fan_in_agent.name)
|
|
235
|
+
elif agent.agent_type == AgentType.ROUTER:
|
|
236
|
+
if hasattr(agent, "routing_agents") and agent.routing_agents:
|
|
237
|
+
for child_agent in agent.routing_agents:
|
|
238
|
+
child_agents.add(child_agent.name)
|
|
239
|
+
elif hasattr(agent, "agents") and agent.agents:
|
|
240
|
+
for child_agent in agent.agents:
|
|
241
|
+
child_agents.add(child_agent.name)
|
|
242
|
+
except Exception:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
# Second pass: display agents (parents with children, standalone agents without children)
|
|
246
|
+
for agent_name in sorted(available_agents):
|
|
247
|
+
# Skip if this agent is a child of another agent
|
|
248
|
+
if agent_name in child_agents:
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
if agent_provider is None:
|
|
253
|
+
continue
|
|
254
|
+
agent = agent_provider._agent(agent_name)
|
|
255
|
+
|
|
256
|
+
# Display parent agent
|
|
257
|
+
await _display_agent_info_helper(agent_name, agent_provider)
|
|
258
|
+
|
|
259
|
+
# If it's a workflow agent, display its children
|
|
260
|
+
if agent.agent_type == AgentType.PARALLEL:
|
|
261
|
+
await _display_parallel_children(agent, agent_provider)
|
|
262
|
+
elif agent.agent_type == AgentType.ROUTER:
|
|
263
|
+
await _display_router_children(agent, agent_provider)
|
|
264
|
+
|
|
265
|
+
except Exception:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
async def _display_parallel_children(parallel_agent, agent_provider: "AgentApp | None") -> None:
|
|
270
|
+
"""Display child agents of a parallel agent in tree format."""
|
|
271
|
+
children = []
|
|
272
|
+
|
|
273
|
+
# Collect fan-out agents
|
|
274
|
+
if hasattr(parallel_agent, "fan_out_agents") and parallel_agent.fan_out_agents:
|
|
275
|
+
for child_agent in parallel_agent.fan_out_agents:
|
|
276
|
+
children.append(child_agent)
|
|
277
|
+
|
|
278
|
+
# Collect fan-in agent
|
|
279
|
+
if hasattr(parallel_agent, "fan_in_agent") and parallel_agent.fan_in_agent:
|
|
280
|
+
children.append(parallel_agent.fan_in_agent)
|
|
281
|
+
|
|
282
|
+
# Display children with tree formatting
|
|
283
|
+
for i, child_agent in enumerate(children):
|
|
284
|
+
is_last = i == len(children) - 1
|
|
285
|
+
prefix = "└─" if is_last else "├─"
|
|
286
|
+
await _display_child_agent_info(child_agent, prefix, agent_provider)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def _display_router_children(router_agent, agent_provider: "AgentApp | None") -> None:
|
|
290
|
+
"""Display child agents of a router agent in tree format."""
|
|
291
|
+
children = []
|
|
292
|
+
|
|
293
|
+
# Collect routing agents
|
|
294
|
+
if hasattr(router_agent, "routing_agents") and router_agent.routing_agents:
|
|
295
|
+
children = router_agent.routing_agents
|
|
296
|
+
elif hasattr(router_agent, "agents") and router_agent.agents:
|
|
297
|
+
children = router_agent.agents
|
|
298
|
+
|
|
299
|
+
# Display children with tree formatting
|
|
300
|
+
for i, child_agent in enumerate(children):
|
|
301
|
+
is_last = i == len(children) - 1
|
|
302
|
+
prefix = "└─" if is_last else "├─"
|
|
303
|
+
await _display_child_agent_info(child_agent, prefix, agent_provider)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
async def _display_child_agent_info(
|
|
307
|
+
child_agent, prefix: str, agent_provider: "AgentApp | None"
|
|
308
|
+
) -> None:
|
|
309
|
+
"""Display info for a child agent with tree prefix."""
|
|
310
|
+
try:
|
|
311
|
+
# Get counts for child agent
|
|
312
|
+
servers = await child_agent.list_servers()
|
|
313
|
+
server_count = len(servers) if servers else 0
|
|
314
|
+
|
|
315
|
+
tools_result = await child_agent.list_tools()
|
|
316
|
+
tool_count = (
|
|
317
|
+
len(tools_result.tools) if tools_result and hasattr(tools_result, "tools") else 0
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
resources_dict = await child_agent.list_resources()
|
|
321
|
+
resource_count = (
|
|
322
|
+
sum(len(resources) for resources in resources_dict.values()) if resources_dict else 0
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
prompts_dict = await child_agent.list_prompts()
|
|
326
|
+
prompt_count = sum(len(prompts) for prompts in prompts_dict.values()) if prompts_dict else 0
|
|
327
|
+
|
|
328
|
+
# Only display if child has MCP servers
|
|
329
|
+
if server_count > 0:
|
|
330
|
+
# Pluralization helpers
|
|
331
|
+
server_word = "Server" if server_count == 1 else "Servers"
|
|
332
|
+
tool_word = "tool" if tool_count == 1 else "tools"
|
|
333
|
+
resource_word = "resource" if resource_count == 1 else "resources"
|
|
334
|
+
prompt_word = "prompt" if prompt_count == 1 else "prompts"
|
|
335
|
+
|
|
336
|
+
rich_print(
|
|
337
|
+
f"[dim] {prefix} [/dim][blue]{child_agent.name}[/blue][dim]:[/dim] {server_count:,}[dim] MCP {server_word}, [/dim]{tool_count:,}[dim] {tool_word}, [/dim]{resource_count:,}[dim] {resource_word}, [/dim]{prompt_count:,}[dim] {prompt_word} available[/dim]"
|
|
338
|
+
)
|
|
339
|
+
else:
|
|
340
|
+
# Show child even without MCP servers for context
|
|
341
|
+
rich_print(
|
|
342
|
+
f"[dim] {prefix} [/dim][blue]{child_agent.name}[/blue][dim]: No MCP Servers[/dim]"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
except Exception:
|
|
346
|
+
# Fallback: just show the name
|
|
347
|
+
rich_print(f"[dim] {prefix} [/dim][blue]{child_agent.name}[/blue]")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class AgentCompleter(Completer):
|
|
351
|
+
"""Provide completion for agent names and common commands."""
|
|
352
|
+
|
|
353
|
+
def __init__(
|
|
354
|
+
self,
|
|
355
|
+
agents: list[str],
|
|
356
|
+
commands: list[str] = None,
|
|
357
|
+
agent_types: dict = None,
|
|
358
|
+
is_human_input: bool = False,
|
|
359
|
+
) -> None:
|
|
360
|
+
self.agents = agents
|
|
361
|
+
# Map commands to their descriptions for better completion hints
|
|
362
|
+
self.commands = {
|
|
363
|
+
"mcp": "Show MCP server status",
|
|
364
|
+
"history": "Show conversation history overview (optionally another agent)",
|
|
365
|
+
"tools": "List available MCP Tools",
|
|
366
|
+
"skills": "List available Agent Skills",
|
|
367
|
+
"prompt": "List and choose MCP prompts, or apply specific prompt (/prompt <name>)",
|
|
368
|
+
"clear": "Clear history",
|
|
369
|
+
"clear last": "Remove the most recent message from history",
|
|
370
|
+
"agents": "List available agents",
|
|
371
|
+
"system": "Show the current system prompt",
|
|
372
|
+
"usage": "Show current usage statistics",
|
|
373
|
+
"markdown": "Show last assistant message without markdown formatting",
|
|
374
|
+
"save_history": "Save history; .json = MCP JSON, others = Markdown",
|
|
375
|
+
"load_history": "Load history from a file",
|
|
376
|
+
"help": "Show commands and shortcuts",
|
|
377
|
+
"EXIT": "Exit fast-agent, terminating any running workflows",
|
|
378
|
+
"STOP": "Stop this prompting session and move to next workflow step",
|
|
379
|
+
**(commands or {}), # Allow custom commands to be passed in
|
|
380
|
+
}
|
|
381
|
+
if is_human_input:
|
|
382
|
+
self.commands.pop("agents")
|
|
383
|
+
self.commands.pop("prompt", None) # Remove prompt command in human input mode
|
|
384
|
+
self.commands.pop("tools", None) # Remove tools command in human input mode
|
|
385
|
+
self.commands.pop("usage", None) # Remove usage command in human input mode
|
|
386
|
+
self.agent_types = agent_types or {}
|
|
387
|
+
|
|
388
|
+
def _complete_history_files(self, partial: str):
|
|
389
|
+
"""Generate completions for history files (.json and .md)."""
|
|
390
|
+
from pathlib import Path
|
|
391
|
+
|
|
392
|
+
# Determine directory and prefix to search
|
|
393
|
+
if partial:
|
|
394
|
+
partial_path = Path(partial)
|
|
395
|
+
if partial.endswith("/") or partial.endswith(os.sep):
|
|
396
|
+
search_dir = partial_path
|
|
397
|
+
prefix = ""
|
|
398
|
+
else:
|
|
399
|
+
search_dir = partial_path.parent if partial_path.parent != partial_path else Path(".")
|
|
400
|
+
prefix = partial_path.name
|
|
401
|
+
else:
|
|
402
|
+
search_dir = Path(".")
|
|
403
|
+
prefix = ""
|
|
404
|
+
|
|
405
|
+
# Ensure search_dir exists
|
|
406
|
+
if not search_dir.exists():
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
# List directory contents
|
|
411
|
+
for entry in sorted(search_dir.iterdir()):
|
|
412
|
+
name = entry.name
|
|
413
|
+
|
|
414
|
+
# Skip hidden files
|
|
415
|
+
if name.startswith("."):
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
# Check if name matches prefix
|
|
419
|
+
if not name.lower().startswith(prefix.lower()):
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
# Build the completion text
|
|
423
|
+
if search_dir == Path("."):
|
|
424
|
+
completion_text = name
|
|
425
|
+
else:
|
|
426
|
+
completion_text = str(search_dir / name)
|
|
427
|
+
|
|
428
|
+
# Handle directories - add trailing slash
|
|
429
|
+
if entry.is_dir():
|
|
430
|
+
yield Completion(
|
|
431
|
+
completion_text + "/",
|
|
432
|
+
start_position=-len(partial),
|
|
433
|
+
display=name + "/",
|
|
434
|
+
display_meta="directory",
|
|
435
|
+
)
|
|
436
|
+
# Handle .json and .md files
|
|
437
|
+
elif entry.is_file() and (name.endswith(".json") or name.endswith(".md")):
|
|
438
|
+
file_type = "JSON history" if name.endswith(".json") else "Markdown"
|
|
439
|
+
yield Completion(
|
|
440
|
+
completion_text,
|
|
441
|
+
start_position=-len(partial),
|
|
442
|
+
display=name,
|
|
443
|
+
display_meta=file_type,
|
|
444
|
+
)
|
|
445
|
+
except PermissionError:
|
|
446
|
+
pass # Skip directories we can't read
|
|
447
|
+
|
|
448
|
+
def get_completions(self, document, complete_event):
|
|
449
|
+
"""Synchronous completions method - this is what prompt_toolkit expects by default"""
|
|
450
|
+
text = document.text_before_cursor
|
|
451
|
+
text_lower = text.lower()
|
|
452
|
+
|
|
453
|
+
# Sub-completion for /load_history - show .json and .md files
|
|
454
|
+
if text_lower.startswith("/load_history ") or text_lower.startswith("/load "):
|
|
455
|
+
# Extract the partial path after the command
|
|
456
|
+
if text_lower.startswith("/load_history "):
|
|
457
|
+
partial = text[len("/load_history "):]
|
|
458
|
+
else:
|
|
459
|
+
partial = text[len("/load "):]
|
|
460
|
+
|
|
461
|
+
yield from self._complete_history_files(partial)
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
# Complete commands
|
|
465
|
+
if text_lower.startswith("/"):
|
|
466
|
+
cmd = text_lower[1:]
|
|
467
|
+
# Simple command completion - match beginning of command
|
|
468
|
+
for command, description in self.commands.items():
|
|
469
|
+
if command.lower().startswith(cmd):
|
|
470
|
+
yield Completion(
|
|
471
|
+
command,
|
|
472
|
+
start_position=-len(cmd),
|
|
473
|
+
display=command,
|
|
474
|
+
display_meta=description,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Complete agent names for agent-related commands
|
|
478
|
+
elif text.startswith("@"):
|
|
479
|
+
agent_name = text[1:]
|
|
480
|
+
for agent in self.agents:
|
|
481
|
+
if agent.lower().startswith(agent_name.lower()):
|
|
482
|
+
# Get agent type or default to "Agent"
|
|
483
|
+
agent_type = self.agent_types.get(agent, AgentType.BASIC).value
|
|
484
|
+
yield Completion(
|
|
485
|
+
agent,
|
|
486
|
+
start_position=-len(agent_name),
|
|
487
|
+
display=agent,
|
|
488
|
+
display_meta=agent_type,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# Helper function to open text in an external editor
|
|
493
|
+
def get_text_from_editor(initial_text: str = "") -> str:
|
|
494
|
+
"""
|
|
495
|
+
Opens the user\'s configured editor ($VISUAL or $EDITOR) to edit the initial_text.
|
|
496
|
+
Falls back to \'nano\' (Unix) or \'notepad\' (Windows) if neither is set.
|
|
497
|
+
Returns the edited text, or the original text if an error occurs.
|
|
498
|
+
"""
|
|
499
|
+
editor_cmd_str = os.environ.get("VISUAL") or os.environ.get("EDITOR")
|
|
500
|
+
|
|
501
|
+
if not editor_cmd_str:
|
|
502
|
+
if os.name == "nt": # Windows
|
|
503
|
+
editor_cmd_str = "notepad"
|
|
504
|
+
else: # Unix-like (Linux, macOS)
|
|
505
|
+
editor_cmd_str = "nano" # A common, usually available, simple editor
|
|
506
|
+
|
|
507
|
+
# Use shlex.split to handle editors with arguments (e.g., "code --wait")
|
|
508
|
+
try:
|
|
509
|
+
editor_cmd_list = shlex.split(editor_cmd_str)
|
|
510
|
+
if not editor_cmd_list: # Handle empty string from shlex.split
|
|
511
|
+
raise ValueError("Editor command string is empty or invalid.")
|
|
512
|
+
except ValueError as e:
|
|
513
|
+
rich_print(f"[red]Error: Invalid editor command string ('{editor_cmd_str}'): {e}[/red]")
|
|
514
|
+
return initial_text
|
|
515
|
+
|
|
516
|
+
# Create a temporary file for the editor to use.
|
|
517
|
+
# Using a suffix can help some editors with syntax highlighting or mode.
|
|
518
|
+
try:
|
|
519
|
+
with tempfile.NamedTemporaryFile(
|
|
520
|
+
mode="w+", delete=False, suffix=".txt", encoding="utf-8"
|
|
521
|
+
) as tmp_file:
|
|
522
|
+
if initial_text:
|
|
523
|
+
tmp_file.write(initial_text)
|
|
524
|
+
tmp_file.flush() # Ensure content is written to disk before editor opens it
|
|
525
|
+
temp_file_path = tmp_file.name
|
|
526
|
+
except Exception as e:
|
|
527
|
+
rich_print(f"[red]Error: Could not create temporary file for editor: {e}[/red]")
|
|
528
|
+
return initial_text
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
# Construct the full command: editor_parts + [temp_file_path]
|
|
532
|
+
# e.g., [\'vim\', \'/tmp/somefile.txt\'] or [\'code\', \'--wait\', \'/tmp/somefile.txt\']
|
|
533
|
+
full_cmd = editor_cmd_list + [temp_file_path]
|
|
534
|
+
|
|
535
|
+
# Run the editor. This is a blocking call.
|
|
536
|
+
subprocess.run(full_cmd, check=True)
|
|
537
|
+
|
|
538
|
+
# Read the content back from the temporary file.
|
|
539
|
+
with open(temp_file_path, "r", encoding="utf-8") as f:
|
|
540
|
+
edited_text = f.read()
|
|
541
|
+
|
|
542
|
+
except FileNotFoundError:
|
|
543
|
+
rich_print(
|
|
544
|
+
f"[red]Error: Editor command '{editor_cmd_list[0]}' not found. "
|
|
545
|
+
f"Please set $VISUAL or $EDITOR correctly, or install '{editor_cmd_list[0]}'.[/red]"
|
|
546
|
+
)
|
|
547
|
+
return initial_text
|
|
548
|
+
except subprocess.CalledProcessError as e:
|
|
549
|
+
rich_print(
|
|
550
|
+
f"[red]Error: Editor '{editor_cmd_list[0]}' closed with an error (code {e.returncode}).[/red]"
|
|
551
|
+
)
|
|
552
|
+
return initial_text
|
|
553
|
+
except Exception as e:
|
|
554
|
+
rich_print(
|
|
555
|
+
f"[red]An unexpected error occurred while launching or using the editor: {e}[/red]"
|
|
556
|
+
)
|
|
557
|
+
return initial_text
|
|
558
|
+
finally:
|
|
559
|
+
# Always attempt to clean up the temporary file.
|
|
560
|
+
if "temp_file_path" in locals() and os.path.exists(temp_file_path):
|
|
561
|
+
try:
|
|
562
|
+
os.remove(temp_file_path)
|
|
563
|
+
except Exception as e:
|
|
564
|
+
rich_print(
|
|
565
|
+
f"[yellow]Warning: Could not remove temporary file {temp_file_path}: {e}[/yellow]"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
return edited_text.strip() # Added strip() to remove trailing newlines often added by editors
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def create_keybindings(
|
|
572
|
+
on_toggle_multiline=None, app=None, agent_provider: "AgentApp | None" = None, agent_name=None
|
|
573
|
+
):
|
|
574
|
+
"""Create custom key bindings."""
|
|
575
|
+
kb = KeyBindings()
|
|
576
|
+
|
|
577
|
+
@kb.add("c-m", filter=Condition(lambda: not in_multiline_mode))
|
|
578
|
+
def _(event) -> None:
|
|
579
|
+
"""Enter: accept input when not in multiline mode."""
|
|
580
|
+
event.current_buffer.validate_and_handle()
|
|
581
|
+
|
|
582
|
+
@kb.add("c-j", filter=Condition(lambda: not in_multiline_mode))
|
|
583
|
+
def _(event) -> None:
|
|
584
|
+
"""Ctrl+J: Insert newline when in normal mode."""
|
|
585
|
+
event.current_buffer.insert_text("\n")
|
|
586
|
+
|
|
587
|
+
@kb.add("c-m", filter=Condition(lambda: in_multiline_mode))
|
|
588
|
+
def _(event) -> None:
|
|
589
|
+
"""Enter: Insert newline when in multiline mode."""
|
|
590
|
+
event.current_buffer.insert_text("\n")
|
|
591
|
+
|
|
592
|
+
# Use c-j (Ctrl+J) as an alternative to represent Ctrl+Enter in multiline mode
|
|
593
|
+
@kb.add("c-j", filter=Condition(lambda: in_multiline_mode))
|
|
594
|
+
def _(event) -> None:
|
|
595
|
+
"""Ctrl+J (equivalent to Ctrl+Enter): Submit in multiline mode."""
|
|
596
|
+
event.current_buffer.validate_and_handle()
|
|
597
|
+
|
|
598
|
+
@kb.add("c-t")
|
|
599
|
+
def _(event) -> None:
|
|
600
|
+
"""Ctrl+T: Toggle multiline mode."""
|
|
601
|
+
global in_multiline_mode
|
|
602
|
+
in_multiline_mode = not in_multiline_mode
|
|
603
|
+
|
|
604
|
+
# Force redraw the app to update toolbar
|
|
605
|
+
if event.app:
|
|
606
|
+
event.app.invalidate()
|
|
607
|
+
elif app:
|
|
608
|
+
app.invalidate()
|
|
609
|
+
|
|
610
|
+
# Call the toggle callback if provided
|
|
611
|
+
if on_toggle_multiline:
|
|
612
|
+
on_toggle_multiline(in_multiline_mode)
|
|
613
|
+
|
|
614
|
+
# Instead of printing, we'll just update the toolbar
|
|
615
|
+
# The toolbar will show the current mode
|
|
616
|
+
|
|
617
|
+
@kb.add("c-l")
|
|
618
|
+
def _(event) -> None:
|
|
619
|
+
"""Ctrl+L: Clear and redraw the terminal screen."""
|
|
620
|
+
app_ref = event.app or app
|
|
621
|
+
if app_ref and getattr(app_ref, "renderer", None):
|
|
622
|
+
app_ref.renderer.clear()
|
|
623
|
+
app_ref.invalidate()
|
|
624
|
+
|
|
625
|
+
@kb.add("c-u")
|
|
626
|
+
def _(event) -> None:
|
|
627
|
+
"""Ctrl+U: Clear the input buffer."""
|
|
628
|
+
event.current_buffer.text = ""
|
|
629
|
+
|
|
630
|
+
@kb.add("c-e")
|
|
631
|
+
async def _(event) -> None:
|
|
632
|
+
"""Ctrl+E: Edit current buffer in $EDITOR."""
|
|
633
|
+
current_text = event.app.current_buffer.text
|
|
634
|
+
try:
|
|
635
|
+
# Run the synchronous editor function in a thread
|
|
636
|
+
edited_text = await event.app.loop.run_in_executor(
|
|
637
|
+
None, get_text_from_editor, current_text
|
|
638
|
+
)
|
|
639
|
+
event.app.current_buffer.text = edited_text
|
|
640
|
+
# Optionally, move cursor to the end of the edited text
|
|
641
|
+
event.app.current_buffer.cursor_position = len(edited_text)
|
|
642
|
+
except asyncio.CancelledError:
|
|
643
|
+
rich_print("[yellow]Editor interaction cancelled.[/yellow]")
|
|
644
|
+
except Exception as e:
|
|
645
|
+
rich_print(f"[red]Error during editor interaction: {e}[/red]")
|
|
646
|
+
finally:
|
|
647
|
+
# Ensure the UI is updated
|
|
648
|
+
if event.app:
|
|
649
|
+
event.app.invalidate()
|
|
650
|
+
|
|
651
|
+
# Store reference to agent provider and agent name for clipboard functionality
|
|
652
|
+
kb.agent_provider = agent_provider
|
|
653
|
+
kb.current_agent_name = agent_name
|
|
654
|
+
|
|
655
|
+
@kb.add("c-y")
|
|
656
|
+
async def _(event) -> None:
|
|
657
|
+
"""Ctrl+Y: Copy last assistant response to clipboard."""
|
|
658
|
+
if kb.agent_provider and kb.current_agent_name:
|
|
659
|
+
try:
|
|
660
|
+
# Get the agent from AgentApp
|
|
661
|
+
agent = kb.agent_provider._agent(kb.current_agent_name)
|
|
662
|
+
|
|
663
|
+
# Find last assistant message
|
|
664
|
+
for msg in reversed(agent.message_history):
|
|
665
|
+
if msg.role == "assistant":
|
|
666
|
+
content = msg.last_text()
|
|
667
|
+
import pyperclip
|
|
668
|
+
|
|
669
|
+
pyperclip.copy(content)
|
|
670
|
+
rich_print("\n[green]✓ Copied to clipboard[/green]")
|
|
671
|
+
return
|
|
672
|
+
except Exception:
|
|
673
|
+
pass
|
|
674
|
+
|
|
675
|
+
return kb
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
async def get_enhanced_input(
|
|
679
|
+
agent_name: str,
|
|
680
|
+
default: str = "",
|
|
681
|
+
show_default: bool = False,
|
|
682
|
+
show_stop_hint: bool = False,
|
|
683
|
+
multiline: bool = False,
|
|
684
|
+
available_agent_names: list[str] = None,
|
|
685
|
+
agent_types: dict[str, AgentType] = None,
|
|
686
|
+
is_human_input: bool = False,
|
|
687
|
+
toolbar_color: str = "ansiblue",
|
|
688
|
+
agent_provider: "AgentApp | None" = None,
|
|
689
|
+
) -> str:
|
|
690
|
+
"""
|
|
691
|
+
Enhanced input with advanced prompt_toolkit features.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
agent_name: Name of the agent (used for prompt and history)
|
|
695
|
+
default: Default value if user presses enter
|
|
696
|
+
show_default: Whether to show the default value in the prompt
|
|
697
|
+
show_stop_hint: Whether to show the STOP hint
|
|
698
|
+
multiline: Start in multiline mode
|
|
699
|
+
available_agent_names: List of agent names for auto-completion
|
|
700
|
+
agent_types: Dictionary mapping agent names to their types for display
|
|
701
|
+
is_human_input: Whether this is a human input request (disables agent selection features)
|
|
702
|
+
toolbar_color: Color to use for the agent name in the toolbar (default: "ansiblue")
|
|
703
|
+
agent_provider: Optional AgentApp for displaying agent info
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
User input string
|
|
707
|
+
"""
|
|
708
|
+
global in_multiline_mode, available_agents, help_message_shown
|
|
709
|
+
|
|
710
|
+
# Update global state
|
|
711
|
+
in_multiline_mode = multiline
|
|
712
|
+
if available_agent_names:
|
|
713
|
+
available_agents = set(available_agent_names)
|
|
714
|
+
|
|
715
|
+
# Get or create history object for this agent
|
|
716
|
+
if agent_name not in agent_histories:
|
|
717
|
+
agent_histories[agent_name] = InMemoryHistory()
|
|
718
|
+
|
|
719
|
+
# Define callback for multiline toggle
|
|
720
|
+
def on_multiline_toggle(enabled) -> None:
|
|
721
|
+
nonlocal session
|
|
722
|
+
if hasattr(session, "app") and session.app:
|
|
723
|
+
session.app.invalidate()
|
|
724
|
+
|
|
725
|
+
# Define toolbar function that will update dynamically
|
|
726
|
+
def get_toolbar():
|
|
727
|
+
if in_multiline_mode:
|
|
728
|
+
mode_style = "ansired" # More noticeable for multiline mode
|
|
729
|
+
mode_text = "MULTILINE"
|
|
730
|
+
# toggle_text = "Normal"
|
|
731
|
+
else:
|
|
732
|
+
mode_style = "ansigreen"
|
|
733
|
+
mode_text = "NORMAL"
|
|
734
|
+
# toggle_text = "Multiline"
|
|
735
|
+
|
|
736
|
+
# No shortcut hints in the toolbar for now
|
|
737
|
+
shortcuts = []
|
|
738
|
+
|
|
739
|
+
# Only show relevant shortcuts based on mode
|
|
740
|
+
shortcuts = [(k, v) for k, v in shortcuts if v]
|
|
741
|
+
|
|
742
|
+
shortcut_text = " | ".join(f"{key}:{action}" for key, action in shortcuts)
|
|
743
|
+
|
|
744
|
+
# Resolve model name, turn counter, and TDV from the current agent if available
|
|
745
|
+
model_display = None
|
|
746
|
+
tdv_segment = None
|
|
747
|
+
turn_count = 0
|
|
748
|
+
agent = None
|
|
749
|
+
if agent_provider:
|
|
750
|
+
try:
|
|
751
|
+
agent = agent_provider._agent(agent_name)
|
|
752
|
+
except Exception as exc:
|
|
753
|
+
print(f"[toolbar debug] unable to resolve agent '{agent_name}': {exc}")
|
|
754
|
+
|
|
755
|
+
if agent:
|
|
756
|
+
for message in agent.message_history:
|
|
757
|
+
if message.role == "user":
|
|
758
|
+
turn_count += 1
|
|
759
|
+
|
|
760
|
+
# Resolve LLM reference safely (avoid assertion when unattached)
|
|
761
|
+
llm = None
|
|
762
|
+
try:
|
|
763
|
+
llm = agent.llm
|
|
764
|
+
except AssertionError:
|
|
765
|
+
llm = getattr(agent, "_llm", None)
|
|
766
|
+
except Exception as exc:
|
|
767
|
+
print(f"[toolbar debug] agent.llm access failed for '{agent_name}': {exc}")
|
|
768
|
+
|
|
769
|
+
model_name = None
|
|
770
|
+
if llm:
|
|
771
|
+
model_name = getattr(llm, "model_name", None)
|
|
772
|
+
if not model_name:
|
|
773
|
+
model_name = getattr(
|
|
774
|
+
getattr(llm, "default_request_params", None), "model", None
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
if not model_name:
|
|
778
|
+
model_name = getattr(agent.config, "model", None)
|
|
779
|
+
if not model_name and getattr(agent.config, "default_request_params", None):
|
|
780
|
+
model_name = getattr(agent.config.default_request_params, "model", None)
|
|
781
|
+
if not model_name:
|
|
782
|
+
context = getattr(agent, "context", None) or getattr(
|
|
783
|
+
agent_provider, "context", None
|
|
784
|
+
)
|
|
785
|
+
config_obj = getattr(context, "config", None) if context else None
|
|
786
|
+
model_name = getattr(config_obj, "default_model", None)
|
|
787
|
+
|
|
788
|
+
if model_name:
|
|
789
|
+
max_len = 25
|
|
790
|
+
model_display = (
|
|
791
|
+
model_name[: max_len - 1] + "…" if len(model_name) > max_len else model_name
|
|
792
|
+
)
|
|
793
|
+
else:
|
|
794
|
+
print(f"[toolbar debug] no model resolved for agent '{agent_name}'")
|
|
795
|
+
model_display = "unknown"
|
|
796
|
+
|
|
797
|
+
# Build TDV capability segment based on model database
|
|
798
|
+
info = None
|
|
799
|
+
if llm:
|
|
800
|
+
info = ModelInfo.from_llm(llm)
|
|
801
|
+
if not info and model_name:
|
|
802
|
+
info = ModelInfo.from_name(model_name)
|
|
803
|
+
|
|
804
|
+
# Default to text-only if info resolution fails for any reason
|
|
805
|
+
t, d, v = (True, False, False)
|
|
806
|
+
if info:
|
|
807
|
+
t, d, v = info.tdv_flags
|
|
808
|
+
|
|
809
|
+
# Check for alert flags in user messages
|
|
810
|
+
alert_flags: set[str] = set()
|
|
811
|
+
error_seen = False
|
|
812
|
+
for message in agent.message_history:
|
|
813
|
+
if message.channels:
|
|
814
|
+
if message.channels.get(FAST_AGENT_ERROR_CHANNEL):
|
|
815
|
+
error_seen = True
|
|
816
|
+
if message.role == "user" and message.channels:
|
|
817
|
+
meta_blocks = message.channels.get(FAST_AGENT_REMOVED_METADATA_CHANNEL, [])
|
|
818
|
+
alert_flags.update(_extract_alert_flags_from_meta(meta_blocks))
|
|
819
|
+
|
|
820
|
+
if error_seen and not alert_flags:
|
|
821
|
+
alert_flags.add("T")
|
|
822
|
+
|
|
823
|
+
def _style_flag(letter: str, supported: bool) -> str:
|
|
824
|
+
# Enabled uses the same color as NORMAL mode (ansigreen), disabled is dim
|
|
825
|
+
if letter in alert_flags:
|
|
826
|
+
return f"<style fg='ansired' bg='ansiblack'>{letter}</style>"
|
|
827
|
+
|
|
828
|
+
enabled_color = "ansigreen"
|
|
829
|
+
if supported:
|
|
830
|
+
return f"<style fg='{enabled_color}' bg='ansiblack'>{letter}</style>"
|
|
831
|
+
return f"<style fg='ansiblack' bg='ansiwhite'>{letter}</style>"
|
|
832
|
+
|
|
833
|
+
tdv_segment = f"{_style_flag('T', t)}{_style_flag('D', d)}{_style_flag('V', v)}"
|
|
834
|
+
else:
|
|
835
|
+
model_display = None
|
|
836
|
+
tdv_segment = None
|
|
837
|
+
|
|
838
|
+
# Build dynamic middle segments: model (in green), turn counter, and optional shortcuts
|
|
839
|
+
middle_segments = []
|
|
840
|
+
if model_display:
|
|
841
|
+
# Model chip + inline TDV flags
|
|
842
|
+
if tdv_segment:
|
|
843
|
+
middle_segments.append(
|
|
844
|
+
f"{tdv_segment} <style bg='ansigreen'>{model_display}</style>"
|
|
845
|
+
)
|
|
846
|
+
else:
|
|
847
|
+
middle_segments.append(f"<style bg='ansigreen'>{model_display}</style>")
|
|
848
|
+
|
|
849
|
+
# Add turn counter (formatted as 3 digits)
|
|
850
|
+
middle_segments.append(f"{turn_count:03d}")
|
|
851
|
+
|
|
852
|
+
if shortcut_text:
|
|
853
|
+
middle_segments.append(shortcut_text)
|
|
854
|
+
middle = " | ".join(middle_segments)
|
|
855
|
+
|
|
856
|
+
# Version/app label in green (dynamic version)
|
|
857
|
+
version_segment = f"fast-agent {app_version}"
|
|
858
|
+
|
|
859
|
+
# Add notifications - prioritize active events over completed ones
|
|
860
|
+
from fast_agent.ui import notification_tracker
|
|
861
|
+
|
|
862
|
+
notification_segment = ""
|
|
863
|
+
|
|
864
|
+
# Check for active events first (highest priority)
|
|
865
|
+
active_status = notification_tracker.get_active_status()
|
|
866
|
+
if active_status:
|
|
867
|
+
event_type = active_status["type"].upper()
|
|
868
|
+
server = active_status["server"]
|
|
869
|
+
notification_segment = (
|
|
870
|
+
f" | <style fg='ansired' bg='ansiblack'>◀ {event_type} ({server})</style>"
|
|
871
|
+
)
|
|
872
|
+
elif notification_tracker.get_count() > 0:
|
|
873
|
+
# Show completed events summary when no active events
|
|
874
|
+
counts_by_type = notification_tracker.get_counts_by_type()
|
|
875
|
+
total_events = sum(counts_by_type.values()) if counts_by_type else 0
|
|
876
|
+
|
|
877
|
+
if len(counts_by_type) == 1:
|
|
878
|
+
event_type, count = next(iter(counts_by_type.items()))
|
|
879
|
+
label_text = notification_tracker.format_event_label(event_type, count)
|
|
880
|
+
notification_segment = f" | ◀ {label_text}"
|
|
881
|
+
else:
|
|
882
|
+
summary = notification_tracker.get_summary(compact=True)
|
|
883
|
+
heading = "event" if total_events == 1 else "events"
|
|
884
|
+
notification_segment = f" | ◀ {total_events} {heading} ({summary})"
|
|
885
|
+
|
|
886
|
+
if middle:
|
|
887
|
+
return HTML(
|
|
888
|
+
f" <style fg='{toolbar_color}' bg='ansiblack'> {agent_name} </style> "
|
|
889
|
+
f" {middle} | <style fg='{mode_style}' bg='ansiblack'> {mode_text} </style> | "
|
|
890
|
+
f"{version_segment}{notification_segment}"
|
|
891
|
+
)
|
|
892
|
+
else:
|
|
893
|
+
return HTML(
|
|
894
|
+
f" <style fg='{toolbar_color}' bg='ansiblack'> {agent_name} </style> "
|
|
895
|
+
f"Mode: <style fg='{mode_style}' bg='ansiblack'> {mode_text} </style> | "
|
|
896
|
+
f"{version_segment}{notification_segment}"
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
# A more terminal-agnostic style that should work across themes
|
|
900
|
+
custom_style = Style.from_dict(
|
|
901
|
+
{
|
|
902
|
+
"completion-menu.completion": "bg:#ansiblack #ansigreen",
|
|
903
|
+
"completion-menu.completion.current": "bg:#ansiblack bold #ansigreen",
|
|
904
|
+
"completion-menu.meta.completion": "bg:#ansiblack #ansiblue",
|
|
905
|
+
"completion-menu.meta.completion.current": "bg:#ansibrightblack #ansiblue",
|
|
906
|
+
"bottom-toolbar": "#ansiblack bg:#ansigray",
|
|
907
|
+
}
|
|
908
|
+
)
|
|
909
|
+
# Create session with history and completions
|
|
910
|
+
session = PromptSession(
|
|
911
|
+
history=agent_histories[agent_name],
|
|
912
|
+
completer=AgentCompleter(
|
|
913
|
+
agents=list(available_agents) if available_agents else [],
|
|
914
|
+
agent_types=agent_types or {},
|
|
915
|
+
is_human_input=is_human_input,
|
|
916
|
+
),
|
|
917
|
+
complete_while_typing=True,
|
|
918
|
+
multiline=Condition(lambda: in_multiline_mode),
|
|
919
|
+
complete_in_thread=True,
|
|
920
|
+
mouse_support=False,
|
|
921
|
+
bottom_toolbar=get_toolbar,
|
|
922
|
+
style=custom_style,
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
# Create key bindings with a reference to the app
|
|
926
|
+
bindings = create_keybindings(
|
|
927
|
+
on_toggle_multiline=on_multiline_toggle,
|
|
928
|
+
app=session.app,
|
|
929
|
+
agent_provider=agent_provider,
|
|
930
|
+
agent_name=agent_name,
|
|
931
|
+
)
|
|
932
|
+
session.app.key_bindings = bindings
|
|
933
|
+
|
|
934
|
+
shell_agent = None
|
|
935
|
+
shell_enabled = False
|
|
936
|
+
shell_access_modes: tuple[str, ...] = ()
|
|
937
|
+
shell_name: str | None = None
|
|
938
|
+
if agent_provider:
|
|
939
|
+
try:
|
|
940
|
+
shell_agent = agent_provider._agent(agent_name)
|
|
941
|
+
except Exception:
|
|
942
|
+
shell_agent = None
|
|
943
|
+
|
|
944
|
+
if shell_agent:
|
|
945
|
+
shell_enabled = bool(getattr(shell_agent, "_shell_runtime_enabled", False))
|
|
946
|
+
modes_attr = getattr(shell_agent, "_shell_access_modes", ())
|
|
947
|
+
if isinstance(modes_attr, (list, tuple)):
|
|
948
|
+
shell_access_modes = tuple(str(mode) for mode in modes_attr)
|
|
949
|
+
elif modes_attr:
|
|
950
|
+
shell_access_modes = (str(modes_attr),)
|
|
951
|
+
|
|
952
|
+
# Get the detected shell name from the runtime
|
|
953
|
+
if shell_enabled:
|
|
954
|
+
shell_runtime = getattr(shell_agent, "_shell_runtime", None)
|
|
955
|
+
if shell_runtime:
|
|
956
|
+
runtime_info = shell_runtime.runtime_info()
|
|
957
|
+
shell_name = runtime_info.get("name")
|
|
958
|
+
|
|
959
|
+
# Create formatted prompt text
|
|
960
|
+
arrow_segment = "<ansibrightyellow>❯</ansibrightyellow>" if shell_enabled else "❯"
|
|
961
|
+
prompt_text = f"<ansibrightblue>{agent_name}</ansibrightblue> {arrow_segment} "
|
|
962
|
+
|
|
963
|
+
# Add default value display if requested
|
|
964
|
+
if show_default and default and default != "STOP":
|
|
965
|
+
prompt_text = f"{prompt_text} [<ansigreen>{default}</ansigreen>] "
|
|
966
|
+
|
|
967
|
+
# Only show hints at startup if requested
|
|
968
|
+
if show_stop_hint:
|
|
969
|
+
if default == "STOP":
|
|
970
|
+
rich_print("Enter a prompt, [red]STOP[/red] to finish")
|
|
971
|
+
if default:
|
|
972
|
+
rich_print(f"Press <ENTER> to use the default prompt:\n[cyan]{default}[/cyan]")
|
|
973
|
+
|
|
974
|
+
# Mention available features but only on first usage globally
|
|
975
|
+
if not help_message_shown:
|
|
976
|
+
if is_human_input:
|
|
977
|
+
rich_print("[dim]Type /help for commands. Ctrl+T toggles multiline mode.[/dim]")
|
|
978
|
+
else:
|
|
979
|
+
rich_print(
|
|
980
|
+
"[dim]Type '/' for commands, '@' to switch agent. Ctrl+T multiline, CTRL+E external editor.[/dim]\n"
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
# Display agent info right after help text if agent_provider is available
|
|
984
|
+
if agent_provider and not is_human_input:
|
|
985
|
+
# Display info for all available agents with tree structure for workflows
|
|
986
|
+
await _display_all_agents_with_hierarchy(available_agents, agent_provider)
|
|
987
|
+
|
|
988
|
+
# Show streaming status message
|
|
989
|
+
if agent_provider:
|
|
990
|
+
# Get logger settings from the agent's context (not agent_provider)
|
|
991
|
+
logger_settings = None
|
|
992
|
+
try:
|
|
993
|
+
active_agent = shell_agent
|
|
994
|
+
if active_agent is None:
|
|
995
|
+
active_agent = agent_provider._agent(agent_name)
|
|
996
|
+
agent_context = active_agent._context or active_agent.context
|
|
997
|
+
logger_settings = agent_context.config.logger
|
|
998
|
+
except Exception:
|
|
999
|
+
# If we can't get the agent or its context, logger_settings stays None
|
|
1000
|
+
pass
|
|
1001
|
+
|
|
1002
|
+
# Only show streaming messages if chat display is enabled AND we have logger_settings
|
|
1003
|
+
if logger_settings:
|
|
1004
|
+
show_chat = getattr(logger_settings, "show_chat", True)
|
|
1005
|
+
|
|
1006
|
+
if show_chat:
|
|
1007
|
+
# Check for parallel agents
|
|
1008
|
+
has_parallel = any(
|
|
1009
|
+
agent.agent_type == AgentType.PARALLEL
|
|
1010
|
+
for agent in agent_provider._agents.values()
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
# Note: streaming may have been disabled by fastagent.py if parallel agents exist
|
|
1014
|
+
# So we check has_parallel first to show the appropriate message
|
|
1015
|
+
if has_parallel:
|
|
1016
|
+
# Streaming is disabled due to parallel agents
|
|
1017
|
+
rich_print(
|
|
1018
|
+
"[dim]Markdown Streaming disabled (Parallel Agents configured)[/dim]"
|
|
1019
|
+
)
|
|
1020
|
+
else:
|
|
1021
|
+
# Check if streaming is enabled
|
|
1022
|
+
streaming_enabled = getattr(logger_settings, "streaming_display", True)
|
|
1023
|
+
streaming_mode = getattr(logger_settings, "streaming", "markdown")
|
|
1024
|
+
if streaming_enabled and streaming_mode != "none":
|
|
1025
|
+
# Streaming is enabled - notify users since it's experimental
|
|
1026
|
+
rich_print(
|
|
1027
|
+
f"[dim]Experimental: Streaming Enabled - {streaming_mode} mode[/dim]"
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
# Show model source if configured via env var or config file
|
|
1031
|
+
model_source = getattr(agent_context.config, "model_source", None)
|
|
1032
|
+
if model_source:
|
|
1033
|
+
rich_print(f"[dim]Model selected via {model_source}[/dim]")
|
|
1034
|
+
|
|
1035
|
+
# Show HuggingFace model and provider info if applicable
|
|
1036
|
+
try:
|
|
1037
|
+
if active_agent.llm:
|
|
1038
|
+
get_hf_info = getattr(active_agent.llm, "get_hf_display_info", None)
|
|
1039
|
+
if get_hf_info:
|
|
1040
|
+
hf_info = get_hf_info()
|
|
1041
|
+
model = hf_info.get("model", "unknown")
|
|
1042
|
+
provider = hf_info.get("provider", "auto-routing")
|
|
1043
|
+
rich_print(
|
|
1044
|
+
f"[dim]HuggingFace: {model} via {provider}[/dim]"
|
|
1045
|
+
)
|
|
1046
|
+
except Exception:
|
|
1047
|
+
pass
|
|
1048
|
+
|
|
1049
|
+
if shell_enabled:
|
|
1050
|
+
modes_display = ", ".join(shell_access_modes or ("direct",))
|
|
1051
|
+
shell_display = f"{modes_display}, {shell_name}" if shell_name else modes_display
|
|
1052
|
+
|
|
1053
|
+
# Add working directory info
|
|
1054
|
+
shell_runtime = getattr(shell_agent, "_shell_runtime", None)
|
|
1055
|
+
if shell_runtime:
|
|
1056
|
+
working_dir = shell_runtime.working_directory()
|
|
1057
|
+
try:
|
|
1058
|
+
# Try to show relative to cwd for cleaner display
|
|
1059
|
+
working_dir_display = str(working_dir.relative_to(Path.cwd()))
|
|
1060
|
+
if working_dir_display == ".":
|
|
1061
|
+
# Show last 2 parts of the path (e.g., "source/fast-agent")
|
|
1062
|
+
parts = Path.cwd().parts
|
|
1063
|
+
if len(parts) >= 2:
|
|
1064
|
+
working_dir_display = "/".join(parts[-2:])
|
|
1065
|
+
elif len(parts) == 1:
|
|
1066
|
+
working_dir_display = parts[0]
|
|
1067
|
+
else:
|
|
1068
|
+
working_dir_display = str(Path.cwd())
|
|
1069
|
+
except ValueError:
|
|
1070
|
+
# If not relative to cwd, show absolute path
|
|
1071
|
+
working_dir_display = str(working_dir)
|
|
1072
|
+
shell_display = f"{shell_display} | cwd: {working_dir_display}"
|
|
1073
|
+
|
|
1074
|
+
rich_print(f"[yellow]Shell Access ({shell_display})[/yellow]")
|
|
1075
|
+
|
|
1076
|
+
rich_print()
|
|
1077
|
+
help_message_shown = True
|
|
1078
|
+
|
|
1079
|
+
# Process special commands
|
|
1080
|
+
|
|
1081
|
+
def pre_process_input(text):
|
|
1082
|
+
# Command processing
|
|
1083
|
+
if text and text.startswith("/"):
|
|
1084
|
+
if text == "/":
|
|
1085
|
+
return ""
|
|
1086
|
+
cmd_parts = text[1:].strip().split(maxsplit=1)
|
|
1087
|
+
cmd = cmd_parts[0].lower()
|
|
1088
|
+
|
|
1089
|
+
if cmd == "help":
|
|
1090
|
+
return "HELP"
|
|
1091
|
+
elif cmd == "agents":
|
|
1092
|
+
return "LIST_AGENTS"
|
|
1093
|
+
elif cmd == "system":
|
|
1094
|
+
return "SHOW_SYSTEM"
|
|
1095
|
+
elif cmd == "usage":
|
|
1096
|
+
return "SHOW_USAGE"
|
|
1097
|
+
elif cmd == "history":
|
|
1098
|
+
target_agent = None
|
|
1099
|
+
if len(cmd_parts) > 1:
|
|
1100
|
+
candidate = cmd_parts[1].strip()
|
|
1101
|
+
if candidate:
|
|
1102
|
+
target_agent = candidate
|
|
1103
|
+
return {"show_history": {"agent": target_agent}}
|
|
1104
|
+
elif cmd == "clear":
|
|
1105
|
+
target_agent = None
|
|
1106
|
+
if len(cmd_parts) > 1:
|
|
1107
|
+
remainder = cmd_parts[1].strip()
|
|
1108
|
+
if remainder:
|
|
1109
|
+
tokens = remainder.split(maxsplit=1)
|
|
1110
|
+
if tokens and tokens[0].lower() == "last":
|
|
1111
|
+
if len(tokens) > 1:
|
|
1112
|
+
candidate = tokens[1].strip()
|
|
1113
|
+
if candidate:
|
|
1114
|
+
target_agent = candidate
|
|
1115
|
+
return {"clear_last": {"agent": target_agent}}
|
|
1116
|
+
target_agent = remainder
|
|
1117
|
+
return {"clear_history": {"agent": target_agent}}
|
|
1118
|
+
elif cmd == "markdown":
|
|
1119
|
+
return "MARKDOWN"
|
|
1120
|
+
elif cmd in ("save_history", "save"):
|
|
1121
|
+
# Return a structured action for the interactive loop to handle
|
|
1122
|
+
# Prefer programmatic saving via HistoryExporter; fall back to magic-string there if needed
|
|
1123
|
+
filename = (
|
|
1124
|
+
cmd_parts[1].strip() if len(cmd_parts) > 1 and cmd_parts[1].strip() else None
|
|
1125
|
+
)
|
|
1126
|
+
return {"save_history": True, "filename": filename}
|
|
1127
|
+
elif cmd in ("load_history", "load"):
|
|
1128
|
+
# Return a structured action for loading history from a file
|
|
1129
|
+
filename = (
|
|
1130
|
+
cmd_parts[1].strip() if len(cmd_parts) > 1 and cmd_parts[1].strip() else None
|
|
1131
|
+
)
|
|
1132
|
+
if not filename:
|
|
1133
|
+
return {"load_history": True, "error": "Filename required for load_history"}
|
|
1134
|
+
return {"load_history": True, "filename": filename}
|
|
1135
|
+
elif cmd in ("mcpstatus", "mcp"):
|
|
1136
|
+
return {"show_mcp_status": True}
|
|
1137
|
+
elif cmd == "prompt":
|
|
1138
|
+
# Handle /prompt with no arguments as interactive mode
|
|
1139
|
+
if len(cmd_parts) > 1:
|
|
1140
|
+
# Direct prompt selection with name or number
|
|
1141
|
+
prompt_arg = cmd_parts[1].strip()
|
|
1142
|
+
# Check if it's a number (use as index) or a name (use directly)
|
|
1143
|
+
if prompt_arg.isdigit():
|
|
1144
|
+
return {"select_prompt": True, "prompt_index": int(prompt_arg)}
|
|
1145
|
+
else:
|
|
1146
|
+
return f"SELECT_PROMPT:{prompt_arg}"
|
|
1147
|
+
else:
|
|
1148
|
+
# If /prompt is used without arguments, show interactive selection
|
|
1149
|
+
return {"select_prompt": True, "prompt_name": None}
|
|
1150
|
+
elif cmd == "tools":
|
|
1151
|
+
# Return a dictionary with list_tools action
|
|
1152
|
+
return {"list_tools": True}
|
|
1153
|
+
elif cmd == "skills":
|
|
1154
|
+
return {"list_skills": True}
|
|
1155
|
+
elif cmd == "exit":
|
|
1156
|
+
return "EXIT"
|
|
1157
|
+
elif cmd.lower() == "stop":
|
|
1158
|
+
return "STOP"
|
|
1159
|
+
|
|
1160
|
+
# Agent switching
|
|
1161
|
+
if text and text.startswith("@"):
|
|
1162
|
+
return f"SWITCH:{text[1:].strip()}"
|
|
1163
|
+
|
|
1164
|
+
# Remove the # command handling completely
|
|
1165
|
+
|
|
1166
|
+
return text
|
|
1167
|
+
|
|
1168
|
+
# Get the input - using async version
|
|
1169
|
+
try:
|
|
1170
|
+
result = await session.prompt_async(HTML(prompt_text), default=default)
|
|
1171
|
+
return pre_process_input(result)
|
|
1172
|
+
except KeyboardInterrupt:
|
|
1173
|
+
# Handle Ctrl+C gracefully
|
|
1174
|
+
return "STOP"
|
|
1175
|
+
except EOFError:
|
|
1176
|
+
# Handle Ctrl+D gracefully
|
|
1177
|
+
return "STOP"
|
|
1178
|
+
except Exception as e:
|
|
1179
|
+
# Log and gracefully handle other exceptions
|
|
1180
|
+
print(f"\nInput error: {type(e).__name__}: {e}")
|
|
1181
|
+
return "STOP"
|
|
1182
|
+
finally:
|
|
1183
|
+
# Ensure the prompt session is properly cleaned up
|
|
1184
|
+
# This is especially important on Windows to prevent resource leaks
|
|
1185
|
+
if session.app.is_running:
|
|
1186
|
+
session.app.exit()
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
async def get_selection_input(
|
|
1190
|
+
prompt_text: str,
|
|
1191
|
+
options: list[str] = None,
|
|
1192
|
+
default: str = None,
|
|
1193
|
+
allow_cancel: bool = True,
|
|
1194
|
+
complete_options: bool = True,
|
|
1195
|
+
) -> str | None:
|
|
1196
|
+
"""
|
|
1197
|
+
Display a selection prompt and return the user's selection.
|
|
1198
|
+
|
|
1199
|
+
Args:
|
|
1200
|
+
prompt_text: Text to display as the prompt
|
|
1201
|
+
options: List of valid options (for auto-completion)
|
|
1202
|
+
default: Default value if user presses enter
|
|
1203
|
+
allow_cancel: Whether to allow cancellation with empty input
|
|
1204
|
+
complete_options: Whether to use the options for auto-completion
|
|
1205
|
+
|
|
1206
|
+
Returns:
|
|
1207
|
+
Selected value, or None if cancelled
|
|
1208
|
+
"""
|
|
1209
|
+
try:
|
|
1210
|
+
# Initialize completer if options provided and completion requested
|
|
1211
|
+
completer = WordCompleter(options) if options and complete_options else None
|
|
1212
|
+
|
|
1213
|
+
# Create prompt session
|
|
1214
|
+
prompt_session = PromptSession(completer=completer)
|
|
1215
|
+
|
|
1216
|
+
try:
|
|
1217
|
+
# Get user input
|
|
1218
|
+
selection = await prompt_session.prompt_async(prompt_text, default=default or "")
|
|
1219
|
+
|
|
1220
|
+
# Handle cancellation
|
|
1221
|
+
if allow_cancel and not selection.strip():
|
|
1222
|
+
return None
|
|
1223
|
+
|
|
1224
|
+
return selection
|
|
1225
|
+
finally:
|
|
1226
|
+
# Ensure prompt session cleanup
|
|
1227
|
+
if prompt_session.app.is_running:
|
|
1228
|
+
prompt_session.app.exit()
|
|
1229
|
+
except (KeyboardInterrupt, EOFError):
|
|
1230
|
+
return None
|
|
1231
|
+
except Exception as e:
|
|
1232
|
+
rich_print(f"\n[red]Error getting selection: {e}[/red]")
|
|
1233
|
+
return None
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
async def get_argument_input(
|
|
1237
|
+
arg_name: str,
|
|
1238
|
+
description: str = None,
|
|
1239
|
+
required: bool = True,
|
|
1240
|
+
) -> str | None:
|
|
1241
|
+
"""
|
|
1242
|
+
Prompt for an argument value with formatting and help text.
|
|
1243
|
+
|
|
1244
|
+
Args:
|
|
1245
|
+
arg_name: Name of the argument
|
|
1246
|
+
description: Optional description of the argument
|
|
1247
|
+
required: Whether this argument is required
|
|
1248
|
+
|
|
1249
|
+
Returns:
|
|
1250
|
+
Input value, or None if cancelled/skipped
|
|
1251
|
+
"""
|
|
1252
|
+
# Format the prompt differently based on whether it's required
|
|
1253
|
+
required_text = "(required)" if required else "(optional, press Enter to skip)"
|
|
1254
|
+
|
|
1255
|
+
# Show description if available
|
|
1256
|
+
if description:
|
|
1257
|
+
rich_print(f" [dim]{arg_name}: {description}[/dim]")
|
|
1258
|
+
|
|
1259
|
+
prompt_text = HTML(
|
|
1260
|
+
f"Enter value for <ansibrightcyan>{arg_name}</ansibrightcyan> {required_text}: "
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
# Create prompt session
|
|
1264
|
+
prompt_session = PromptSession()
|
|
1265
|
+
|
|
1266
|
+
try:
|
|
1267
|
+
# Get user input
|
|
1268
|
+
arg_value = await prompt_session.prompt_async(prompt_text)
|
|
1269
|
+
|
|
1270
|
+
# For optional arguments, empty input means skip
|
|
1271
|
+
if not required and not arg_value:
|
|
1272
|
+
return None
|
|
1273
|
+
|
|
1274
|
+
return arg_value
|
|
1275
|
+
except (KeyboardInterrupt, EOFError):
|
|
1276
|
+
return None
|
|
1277
|
+
except Exception as e:
|
|
1278
|
+
rich_print(f"\n[red]Error getting input: {e}[/red]")
|
|
1279
|
+
return None
|
|
1280
|
+
finally:
|
|
1281
|
+
# Ensure prompt session cleanup
|
|
1282
|
+
if prompt_session.app.is_running:
|
|
1283
|
+
prompt_session.app.exit()
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
async def handle_special_commands(
|
|
1287
|
+
command: Any, agent_app: "AgentApp | None" = None
|
|
1288
|
+
) -> bool | dict[str, Any]:
|
|
1289
|
+
"""
|
|
1290
|
+
Handle special input commands.
|
|
1291
|
+
|
|
1292
|
+
Args:
|
|
1293
|
+
command: The command to handle, can be string or dictionary
|
|
1294
|
+
agent_app: Optional agent app reference
|
|
1295
|
+
|
|
1296
|
+
Returns:
|
|
1297
|
+
True if command was handled, False if not, or a dict with action info
|
|
1298
|
+
"""
|
|
1299
|
+
# Quick guard for empty or None commands
|
|
1300
|
+
if not command:
|
|
1301
|
+
return False
|
|
1302
|
+
|
|
1303
|
+
# If command is already a dictionary, it has been pre-processed
|
|
1304
|
+
# Just return it directly (like when /prompts converts to select_prompt dict)
|
|
1305
|
+
if isinstance(command, dict):
|
|
1306
|
+
return command
|
|
1307
|
+
|
|
1308
|
+
global agent_histories
|
|
1309
|
+
|
|
1310
|
+
# Check for special string commands
|
|
1311
|
+
if command == "HELP":
|
|
1312
|
+
rich_print("\n[bold]Available Commands:[/bold]")
|
|
1313
|
+
rich_print(" /help - Show this help")
|
|
1314
|
+
rich_print(" /agents - List available agents")
|
|
1315
|
+
rich_print(" /system - Show the current system prompt")
|
|
1316
|
+
rich_print(" /prompt <name> - Apply a specific prompt by name")
|
|
1317
|
+
rich_print(" /usage - Show current usage statistics")
|
|
1318
|
+
rich_print(" /skills - List local skills for the active agent")
|
|
1319
|
+
rich_print(" /history [agent_name] - Show chat history overview")
|
|
1320
|
+
rich_print(" /clear [agent_name] - Clear conversation history (keeps templates)")
|
|
1321
|
+
rich_print(" /clear last [agent_name] - Remove the most recent message from history")
|
|
1322
|
+
rich_print(" /markdown - Show last assistant message without markdown formatting")
|
|
1323
|
+
rich_print(" /mcpstatus - Show MCP server status summary for the active agent")
|
|
1324
|
+
rich_print(" /save_history [filename] - Save current chat history to a file")
|
|
1325
|
+
rich_print(
|
|
1326
|
+
" [dim]Tip: Use a .json extension for MCP-compatible JSON; any other extension saves Markdown.[/dim]"
|
|
1327
|
+
)
|
|
1328
|
+
rich_print(
|
|
1329
|
+
" [dim]Default: Timestamped filename (e.g., 25_01_15_14_30-conversation.json)[/dim]"
|
|
1330
|
+
)
|
|
1331
|
+
rich_print(" /load_history <filename> - Load chat history from a file")
|
|
1332
|
+
rich_print(" @agent_name - Switch to agent")
|
|
1333
|
+
rich_print(" STOP - Return control back to the workflow")
|
|
1334
|
+
rich_print(" EXIT - Exit fast-agent, terminating any running workflows")
|
|
1335
|
+
rich_print("\n[bold]Keyboard Shortcuts:[/bold]")
|
|
1336
|
+
rich_print(" Enter - Submit (normal mode) / New line (multiline mode)")
|
|
1337
|
+
rich_print(" Ctrl+Enter - Always submit (in any mode)")
|
|
1338
|
+
rich_print(" Ctrl+T - Toggle multiline mode")
|
|
1339
|
+
rich_print(" Ctrl+E - Edit in external editor")
|
|
1340
|
+
rich_print(" Ctrl+Y - Copy last assistant response to clipboard")
|
|
1341
|
+
rich_print(" Ctrl+L - Redraw the screen")
|
|
1342
|
+
rich_print(" Ctrl+U - Clear input")
|
|
1343
|
+
rich_print(" Up/Down - Navigate history")
|
|
1344
|
+
return True
|
|
1345
|
+
|
|
1346
|
+
elif isinstance(command, str) and command.upper() == "EXIT":
|
|
1347
|
+
raise PromptExitError("User requested to exit fast-agent session")
|
|
1348
|
+
|
|
1349
|
+
elif command == "LIST_AGENTS":
|
|
1350
|
+
if available_agents:
|
|
1351
|
+
rich_print("\n[bold]Available Agents:[/bold]")
|
|
1352
|
+
for agent in sorted(available_agents):
|
|
1353
|
+
rich_print(f" @{agent}")
|
|
1354
|
+
else:
|
|
1355
|
+
rich_print("[yellow]No agents available[/yellow]")
|
|
1356
|
+
return True
|
|
1357
|
+
|
|
1358
|
+
elif command == "SHOW_USAGE":
|
|
1359
|
+
# Return a dictionary to signal that usage should be shown
|
|
1360
|
+
return {"show_usage": True}
|
|
1361
|
+
|
|
1362
|
+
elif command == "SHOW_SYSTEM":
|
|
1363
|
+
# Return a dictionary to signal that system prompt should be shown
|
|
1364
|
+
return {"show_system": True}
|
|
1365
|
+
|
|
1366
|
+
elif command == "MARKDOWN":
|
|
1367
|
+
# Return a dictionary to signal that markdown display should be shown
|
|
1368
|
+
return {"show_markdown": True}
|
|
1369
|
+
|
|
1370
|
+
elif command == "SELECT_PROMPT" or (
|
|
1371
|
+
isinstance(command, str) and command.startswith("SELECT_PROMPT:")
|
|
1372
|
+
):
|
|
1373
|
+
# Handle prompt selection UI
|
|
1374
|
+
if agent_app:
|
|
1375
|
+
# If it's a specific prompt, extract the name
|
|
1376
|
+
prompt_name = None
|
|
1377
|
+
if isinstance(command, str) and command.startswith("SELECT_PROMPT:"):
|
|
1378
|
+
prompt_name = command.split(":", 1)[1].strip()
|
|
1379
|
+
|
|
1380
|
+
# Return a dictionary with a select_prompt action to be handled by the caller
|
|
1381
|
+
return {"select_prompt": True, "prompt_name": prompt_name}
|
|
1382
|
+
else:
|
|
1383
|
+
rich_print(
|
|
1384
|
+
"[yellow]Prompt selection is not available outside of an agent context[/yellow]"
|
|
1385
|
+
)
|
|
1386
|
+
return True
|
|
1387
|
+
|
|
1388
|
+
elif isinstance(command, str) and command.startswith("SWITCH:"):
|
|
1389
|
+
agent_name = command.split(":", 1)[1]
|
|
1390
|
+
if agent_name in available_agents:
|
|
1391
|
+
if agent_app:
|
|
1392
|
+
# The parameter can be the actual agent_app or just True to enable switching
|
|
1393
|
+
return {"switch_agent": agent_name}
|
|
1394
|
+
else:
|
|
1395
|
+
rich_print("[yellow]Agent switching not available in this context[/yellow]")
|
|
1396
|
+
else:
|
|
1397
|
+
rich_print(f"[red]Unknown agent: {agent_name}[/red]")
|
|
1398
|
+
return True
|
|
1399
|
+
|
|
1400
|
+
return False
|