fast-agent-mcp 0.4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,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