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,369 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any, Awaitable, Callable, Literal, Union
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp.tools import Tool as FastMCPTool
|
|
8
|
+
from mcp.types import Tool as McpTool
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from fast_agent.constants import HUMAN_INPUT_TOOL_NAME
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
Human-input (elicitation) tool models, schemas, and builders.
|
|
15
|
+
|
|
16
|
+
This module lives in fast_agent to avoid circular imports and provides:
|
|
17
|
+
- A centralized HUMAN_INPUT_TOOL_NAME constant (imported from fast_agent.constants)
|
|
18
|
+
- Pydantic models for a simplified FormSpec (LLM-friendly)
|
|
19
|
+
- MCP Tool schema builder (sanitized, provider-friendly)
|
|
20
|
+
- FastMCP Tool builder sharing the same schema
|
|
21
|
+
- A lightweight async callback registry for UI-specific elicitation handling
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# -----------------------
|
|
25
|
+
# Pydantic models
|
|
26
|
+
# -----------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OptionItem(BaseModel):
|
|
30
|
+
value: Union[str, int, float, bool]
|
|
31
|
+
label: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FormField(BaseModel):
|
|
35
|
+
name: str
|
|
36
|
+
type: Literal["text", "textarea", "number", "checkbox", "radio"]
|
|
37
|
+
label: str | None = None
|
|
38
|
+
help: str | None = None
|
|
39
|
+
default: Union[str, int, float, bool] | None = None
|
|
40
|
+
required: bool | None = None
|
|
41
|
+
# number constraints
|
|
42
|
+
min: float | None = None
|
|
43
|
+
max: float | None = None
|
|
44
|
+
# select options (for radio)
|
|
45
|
+
options: list[OptionItem] | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HumanFormArgs(BaseModel):
|
|
49
|
+
"""Simplified form spec for human elicitation.
|
|
50
|
+
|
|
51
|
+
Preferred shape for LLMs.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
title: str | None = None
|
|
55
|
+
description: str | None = None
|
|
56
|
+
message: str | None = None
|
|
57
|
+
fields: list[FormField] = Field(default_factory=list, max_length=7)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# -----------------------
|
|
61
|
+
# MCP tool schema builder
|
|
62
|
+
# -----------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_elicitation_tool() -> McpTool:
|
|
66
|
+
"""Build the MCP Tool schema for the elicitation-backed human input tool.
|
|
67
|
+
|
|
68
|
+
Uses Pydantic models to derive a clean, portable JSON Schema suitable for providers.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
schema = HumanFormArgs.model_json_schema()
|
|
72
|
+
|
|
73
|
+
def _resolve_refs(fragment: Any, root: dict[str, Any]) -> Any:
|
|
74
|
+
"""Inline $ref references within a JSON schema fragment using the given root schema.
|
|
75
|
+
|
|
76
|
+
Supports local references of the form '#/$defs/Name'.
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(fragment, dict):
|
|
79
|
+
return fragment
|
|
80
|
+
|
|
81
|
+
if "$ref" in fragment:
|
|
82
|
+
ref_path: str = fragment["$ref"]
|
|
83
|
+
if ref_path.startswith("#/$defs/") and "$defs" in root:
|
|
84
|
+
key = (
|
|
85
|
+
ref_path.split("/#/$defs/")[-1]
|
|
86
|
+
if "/#/$defs/" in ref_path
|
|
87
|
+
else ref_path[len("#/$defs/") :]
|
|
88
|
+
)
|
|
89
|
+
target = root.get("$defs", {}).get(key)
|
|
90
|
+
if isinstance(target, dict):
|
|
91
|
+
return _resolve_refs(target, root)
|
|
92
|
+
fragment = {k: v for k, v in fragment.items() if k != "$ref"}
|
|
93
|
+
fragment.setdefault("type", "object")
|
|
94
|
+
fragment.setdefault("properties", {})
|
|
95
|
+
return fragment
|
|
96
|
+
|
|
97
|
+
resolved: dict[str, Any] = {}
|
|
98
|
+
for k, v in fragment.items():
|
|
99
|
+
if isinstance(v, dict):
|
|
100
|
+
resolved[k] = _resolve_refs(v, root)
|
|
101
|
+
elif isinstance(v, list):
|
|
102
|
+
resolved[k] = [
|
|
103
|
+
_resolve_refs(item, root) if isinstance(item, (dict, list)) else item
|
|
104
|
+
for item in v
|
|
105
|
+
]
|
|
106
|
+
else:
|
|
107
|
+
resolved[k] = v
|
|
108
|
+
return resolved
|
|
109
|
+
|
|
110
|
+
sanitized: dict[str, Any] = {"type": "object"}
|
|
111
|
+
if "properties" in schema:
|
|
112
|
+
props = dict(schema["properties"]) # copy
|
|
113
|
+
if "form_schema" in props:
|
|
114
|
+
props["schema"] = props.pop("form_schema")
|
|
115
|
+
props = _resolve_refs(props, schema)
|
|
116
|
+
sanitized["properties"] = props
|
|
117
|
+
else:
|
|
118
|
+
sanitized["properties"] = {}
|
|
119
|
+
if "required" in schema:
|
|
120
|
+
sanitized["required"] = schema["required"]
|
|
121
|
+
sanitized["additionalProperties"] = True
|
|
122
|
+
|
|
123
|
+
return McpTool(
|
|
124
|
+
name=HUMAN_INPUT_TOOL_NAME,
|
|
125
|
+
description=(
|
|
126
|
+
"Collect structured input from a human via a simple form. "
|
|
127
|
+
"Provide up to 7 fields with types: text, textarea, number, checkbox, or radio. "
|
|
128
|
+
"Each field may include label, help, default; numbers may include min/max; radio may include options (value/label). "
|
|
129
|
+
"You may also add an optional message shown above the form."
|
|
130
|
+
),
|
|
131
|
+
inputSchema=sanitized,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# -----------------------
|
|
136
|
+
# Elicitation input callback registry
|
|
137
|
+
# -----------------------
|
|
138
|
+
|
|
139
|
+
ElicitationCallback = Callable[[dict, str | None, str | None, dict | None], Awaitable[str]]
|
|
140
|
+
|
|
141
|
+
_elicitation_input_callback: ElicitationCallback | None = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def set_elicitation_input_callback(callback: ElicitationCallback) -> None:
|
|
145
|
+
"""Register the UI/backend-specific elicitation handler.
|
|
146
|
+
|
|
147
|
+
The callback should accept a request dict with fields: prompt, description, request_id, metadata,
|
|
148
|
+
plus optional agent_name, server_name, and server_info, and return an awaited string response.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
global _elicitation_input_callback
|
|
152
|
+
_elicitation_input_callback = callback
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_elicitation_input_callback() -> ElicitationCallback | None:
|
|
156
|
+
return _elicitation_input_callback
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# -----------------------
|
|
160
|
+
# Runtime: run the elicitation
|
|
161
|
+
# -----------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def run_elicitation_form(arguments: dict | str, agent_name: str | None = None) -> str:
|
|
165
|
+
"""Parse arguments into a JSON Schema or simplified fields spec and invoke the registered callback.
|
|
166
|
+
|
|
167
|
+
Returns the response string from the callback. Raises if no callback is registered.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def parse_schema_string(val: str) -> dict | None:
|
|
171
|
+
if not isinstance(val, str):
|
|
172
|
+
return None
|
|
173
|
+
s = val.strip()
|
|
174
|
+
if s.startswith("```"):
|
|
175
|
+
lines = s.splitlines()
|
|
176
|
+
if lines:
|
|
177
|
+
lines = lines[1:]
|
|
178
|
+
if lines and lines[-1].strip().startswith("```"):
|
|
179
|
+
lines = lines[:-1]
|
|
180
|
+
s = "\n".join(lines)
|
|
181
|
+
try:
|
|
182
|
+
return json.loads(s)
|
|
183
|
+
except Exception:
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
if not isinstance(arguments, dict):
|
|
187
|
+
if isinstance(arguments, str):
|
|
188
|
+
parsed = parse_schema_string(arguments)
|
|
189
|
+
if isinstance(parsed, dict):
|
|
190
|
+
arguments = parsed
|
|
191
|
+
else:
|
|
192
|
+
raise ValueError("Invalid arguments. Provide FormSpec or JSON Schema object.")
|
|
193
|
+
else:
|
|
194
|
+
raise ValueError("Invalid arguments. Provide FormSpec or JSON Schema object.")
|
|
195
|
+
|
|
196
|
+
schema: dict | None = None
|
|
197
|
+
message: str | None = None
|
|
198
|
+
title: str | None = None
|
|
199
|
+
description: str | None = None
|
|
200
|
+
|
|
201
|
+
if isinstance(arguments.get("fields"), list):
|
|
202
|
+
fields = arguments.get("fields")
|
|
203
|
+
if len(fields) > 7:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"Error: form requests {len(fields)} fields; the maximum allowed is 7."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
properties: dict[str, Any] = {}
|
|
209
|
+
required_fields: list[str] = []
|
|
210
|
+
for field in fields:
|
|
211
|
+
if not isinstance(field, dict):
|
|
212
|
+
continue
|
|
213
|
+
name = field.get("name")
|
|
214
|
+
ftype = field.get("type")
|
|
215
|
+
if not isinstance(name, str) or not isinstance(ftype, str):
|
|
216
|
+
continue
|
|
217
|
+
prop: dict[str, Any] = {}
|
|
218
|
+
label = field.get("label")
|
|
219
|
+
help_text = field.get("help")
|
|
220
|
+
default = field.get("default")
|
|
221
|
+
required_flag = field.get("required")
|
|
222
|
+
|
|
223
|
+
if ftype in ("text", "textarea"):
|
|
224
|
+
prop["type"] = "string"
|
|
225
|
+
elif ftype == "number":
|
|
226
|
+
prop["type"] = "number"
|
|
227
|
+
if isinstance(field.get("min"), (int, float)):
|
|
228
|
+
prop["minimum"] = field.get("min")
|
|
229
|
+
if isinstance(field.get("max"), (int, float)):
|
|
230
|
+
prop["maximum"] = field.get("max")
|
|
231
|
+
elif ftype == "checkbox":
|
|
232
|
+
prop["type"] = "boolean"
|
|
233
|
+
elif ftype == "radio":
|
|
234
|
+
prop["type"] = "string"
|
|
235
|
+
options = field.get("options") or []
|
|
236
|
+
enum_vals = []
|
|
237
|
+
enum_names = []
|
|
238
|
+
for opt in options:
|
|
239
|
+
if isinstance(opt, dict) and "value" in opt:
|
|
240
|
+
enum_vals.append(opt["value"])
|
|
241
|
+
if isinstance(opt.get("label"), str):
|
|
242
|
+
enum_names.append(opt.get("label"))
|
|
243
|
+
elif opt is not None:
|
|
244
|
+
enum_vals.append(opt)
|
|
245
|
+
if enum_vals:
|
|
246
|
+
prop["enum"] = enum_vals
|
|
247
|
+
if enum_names and len(enum_names) == len(enum_vals):
|
|
248
|
+
prop["enumNames"] = enum_names
|
|
249
|
+
else:
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
desc_parts = []
|
|
253
|
+
if isinstance(label, str) and label:
|
|
254
|
+
desc_parts.append(label)
|
|
255
|
+
if isinstance(help_text, str) and help_text:
|
|
256
|
+
desc_parts.append(help_text)
|
|
257
|
+
if desc_parts:
|
|
258
|
+
prop["description"] = " - ".join(desc_parts)
|
|
259
|
+
if default is not None:
|
|
260
|
+
prop["default"] = default
|
|
261
|
+
properties[name] = prop
|
|
262
|
+
if isinstance(required_flag, bool) and required_flag:
|
|
263
|
+
required_fields.append(name)
|
|
264
|
+
|
|
265
|
+
if len(properties) == 0:
|
|
266
|
+
raise ValueError("Invalid form specification: no valid fields provided.")
|
|
267
|
+
|
|
268
|
+
schema = {"type": "object", "properties": properties}
|
|
269
|
+
if required_fields:
|
|
270
|
+
schema["required"] = required_fields
|
|
271
|
+
|
|
272
|
+
title = arguments.get("title") if isinstance(arguments.get("title"), str) else None
|
|
273
|
+
description = (
|
|
274
|
+
arguments.get("description") if isinstance(arguments.get("description"), str) else None
|
|
275
|
+
)
|
|
276
|
+
msg = arguments.get("message")
|
|
277
|
+
if isinstance(msg, str):
|
|
278
|
+
message = msg
|
|
279
|
+
if title:
|
|
280
|
+
schema["title"] = title
|
|
281
|
+
if description:
|
|
282
|
+
schema["description"] = description
|
|
283
|
+
|
|
284
|
+
elif isinstance(arguments.get("schema"), (dict, str)):
|
|
285
|
+
schema = arguments.get("schema")
|
|
286
|
+
if isinstance(schema, str):
|
|
287
|
+
parsed = parse_schema_string(schema)
|
|
288
|
+
if isinstance(parsed, dict):
|
|
289
|
+
schema = parsed
|
|
290
|
+
else:
|
|
291
|
+
raise ValueError("Missing or invalid schema. Provide a JSON Schema object.")
|
|
292
|
+
msg = arguments.get("message")
|
|
293
|
+
if isinstance(msg, str):
|
|
294
|
+
message = msg
|
|
295
|
+
if isinstance(arguments.get("title"), str) and "title" not in schema:
|
|
296
|
+
schema["title"] = arguments.get("title")
|
|
297
|
+
if isinstance(arguments.get("description"), str) and "description" not in schema:
|
|
298
|
+
schema["description"] = arguments.get("description")
|
|
299
|
+
if isinstance(arguments.get("required"), list) and "required" not in schema:
|
|
300
|
+
schema["required"] = arguments.get("required")
|
|
301
|
+
if isinstance(arguments.get("properties"), dict) and "properties" not in schema:
|
|
302
|
+
schema["properties"] = arguments.get("properties")
|
|
303
|
+
|
|
304
|
+
elif ("type" in arguments and "properties" in arguments) or (
|
|
305
|
+
"$schema" in arguments and "properties" in arguments
|
|
306
|
+
):
|
|
307
|
+
schema = arguments
|
|
308
|
+
message = None
|
|
309
|
+
else:
|
|
310
|
+
raise ValueError("Missing or invalid schema or fields in arguments.")
|
|
311
|
+
|
|
312
|
+
props = schema.get("properties", {}) if isinstance(schema.get("properties"), dict) else {}
|
|
313
|
+
if len(props) > 7:
|
|
314
|
+
raise ValueError(f"Error: schema requests {len(props)} fields; the maximum allowed is 7.")
|
|
315
|
+
|
|
316
|
+
request_payload: dict[str, Any] = {
|
|
317
|
+
"prompt": message or schema.get("title") or "Please complete this form:",
|
|
318
|
+
"description": schema.get("description"),
|
|
319
|
+
"request_id": f"__human_input__{uuid.uuid4()}",
|
|
320
|
+
"metadata": {
|
|
321
|
+
"agent_name": agent_name or "Unknown Agent",
|
|
322
|
+
"requested_schema": schema,
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
cb = get_elicitation_input_callback()
|
|
327
|
+
if not cb:
|
|
328
|
+
raise RuntimeError("No elicitation input callback registered")
|
|
329
|
+
|
|
330
|
+
response_text: str = await cb(
|
|
331
|
+
request_payload,
|
|
332
|
+
agent_name or "Unknown Agent",
|
|
333
|
+
"__human_input__",
|
|
334
|
+
None,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return response_text
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# -----------------------
|
|
341
|
+
# FastMCP tool builder
|
|
342
|
+
# -----------------------
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def get_elicitation_fastmcp_tool() -> FastMCPTool:
|
|
346
|
+
async def elicit(
|
|
347
|
+
title: str | None = None,
|
|
348
|
+
description: str | None = None,
|
|
349
|
+
message: str | None = None,
|
|
350
|
+
fields: list[FormField] = Field(default_factory=list, max_length=7),
|
|
351
|
+
) -> str:
|
|
352
|
+
args = {
|
|
353
|
+
"title": title,
|
|
354
|
+
"description": description,
|
|
355
|
+
"message": message,
|
|
356
|
+
"fields": [f.model_dump() if isinstance(f, BaseModel) else f for f in fields],
|
|
357
|
+
}
|
|
358
|
+
return await run_elicitation_form(args)
|
|
359
|
+
|
|
360
|
+
tool = FastMCPTool.from_function(elicit)
|
|
361
|
+
tool.name = HUMAN_INPUT_TOOL_NAME
|
|
362
|
+
tool.description = (
|
|
363
|
+
"Collect structured input from a human via a simple form. Provide up to 7 fields "
|
|
364
|
+
"(text, textarea, number, checkbox, radio). Fields can include label, help, default; "
|
|
365
|
+
"numbers support min/max; radio supports options (value/label); optional message is shown above the form."
|
|
366
|
+
)
|
|
367
|
+
# Harmonize input schema with the sanitized MCP schema for provider compatibility
|
|
368
|
+
tool.parameters = get_elicitation_tool().inputSchema
|
|
369
|
+
return tool
|