mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Add default prefill to FormInput.collect_input (#3937)
This commit is contained in:
parent
8c020adfc6
commit
e4bb6666ab
2 changed files with 82 additions and 6 deletions
|
|
@ -28,7 +28,10 @@ import json
|
|||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
try:
|
||||
import prefab_ui
|
||||
from prefab_ui.actions import SetState
|
||||
from prefab_ui.actions.mcp import CallTool, SendMessage
|
||||
from prefab_ui.app import PrefabApp
|
||||
|
|
@ -49,6 +52,13 @@ except ImportError as _exc:
|
|||
"FormInput requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
|
||||
) from _exc
|
||||
|
||||
# `defaults` kwarg on Form.from_model was added in prefab-ui 0.19.1. Gate on
|
||||
# version so that older prefab-ui keeps working — `default` silently no-ops.
|
||||
try:
|
||||
_FORM_SUPPORTS_DEFAULTS = Version(prefab_ui.__version__) >= Version("0.19.1")
|
||||
except InvalidVersion:
|
||||
_FORM_SUPPORTS_DEFAULTS = False
|
||||
|
||||
import pydantic
|
||||
|
||||
from fastmcp.apps.app import FastMCPApp
|
||||
|
|
@ -161,6 +171,7 @@ class FormInput(FastMCPApp):
|
|||
prompt: str,
|
||||
title: str | None = None,
|
||||
submit_text: str | None = None,
|
||||
default: dict[str, Any] | None = None,
|
||||
) -> PrefabApp:
|
||||
"""Collect structured input from the user.
|
||||
|
||||
|
|
@ -168,6 +179,13 @@ class FormInput(FastMCPApp):
|
|||
prompt: Tell the user what you need and why.
|
||||
title: Optional heading for the form card.
|
||||
submit_text: Optional label for the submit button.
|
||||
default: Optional suggested response — a partial dict of form
|
||||
field values keyed by field name. The form renders with
|
||||
those values pre-filled so the user can confirm or edit
|
||||
rather than start from a blank form. Use this when you
|
||||
already know (or can infer) what the answer should be.
|
||||
Requires prefab-ui>=0.19.1; silently ignored on older
|
||||
versions.
|
||||
"""
|
||||
_title = title or provider._title
|
||||
_submit = submit_text or provider._submit_text
|
||||
|
|
@ -188,16 +206,19 @@ class FormInput(FastMCPApp):
|
|||
SendMessage(RESULT), # ty:ignore[invalid-argument-type]
|
||||
)
|
||||
|
||||
Form.from_model(
|
||||
model,
|
||||
submit_label=_submit,
|
||||
on_submit=[
|
||||
from_model_kwargs: dict[str, Any] = {
|
||||
"submit_label": _submit,
|
||||
"on_submit": [
|
||||
CallTool(
|
||||
"submit_form",
|
||||
on_success=on_success_actions,
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
if default and _FORM_SUPPORTS_DEFAULTS:
|
||||
from_model_kwargs["defaults"] = default
|
||||
|
||||
Form.from_model(model, **from_model_kwargs)
|
||||
|
||||
with CardFooter(), If(STATE.submitted):
|
||||
Muted("Submitted.")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import pydantic
|
|||
import pytest
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.form import FormInput, _backfill_boolean_defaults
|
||||
from fastmcp.apps.form import (
|
||||
_FORM_SUPPORTS_DEFAULTS,
|
||||
FormInput,
|
||||
_backfill_boolean_defaults,
|
||||
)
|
||||
from fastmcp.server.providers.addressing import hashed_backend_name
|
||||
|
||||
|
||||
|
|
@ -144,6 +148,57 @@ class TestFormInputProvider:
|
|||
assert "collect_address" in tool_names
|
||||
|
||||
|
||||
class TestCollectInputDefault:
|
||||
"""The `default` arg pre-fills the form for the user to confirm/edit."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _FORM_SUPPORTS_DEFAULTS,
|
||||
reason="prefab-ui<0.19.1 does not support Form.from_model(defaults=...)",
|
||||
)
|
||||
async def test_default_prefills_form_fields(self):
|
||||
server = FastMCP("test", providers=[FormInput(model=Contact)])
|
||||
|
||||
result = await server.call_tool(
|
||||
"collect_contact",
|
||||
{
|
||||
"prompt": "Confirm your details",
|
||||
"default": {"name": "Alice", "email": "alice@example.com"},
|
||||
},
|
||||
)
|
||||
# The form view should serialize with the prefilled values.
|
||||
payload = json.dumps(result.structured_content)
|
||||
assert "Alice" in payload
|
||||
assert "alice@example.com" in payload
|
||||
|
||||
async def test_default_absent_still_renders_blank_form(self):
|
||||
"""Sanity: omitting `default` keeps the existing blank-form behavior."""
|
||||
server = FastMCP("test", providers=[FormInput(model=Contact)])
|
||||
|
||||
result = await server.call_tool(
|
||||
"collect_contact",
|
||||
{"prompt": "Enter your details"},
|
||||
)
|
||||
assert result.structured_content is not None
|
||||
|
||||
async def test_default_silent_on_older_prefab(self):
|
||||
"""When the sniff says False, `default` is accepted but no-ops.
|
||||
|
||||
This test's real value is that `default` is part of the tool
|
||||
signature regardless of prefab version, so agents can always pass
|
||||
it without runtime errors.
|
||||
"""
|
||||
server = FastMCP("test", providers=[FormInput(model=Contact)])
|
||||
|
||||
result = await server.call_tool(
|
||||
"collect_contact",
|
||||
{
|
||||
"prompt": "Confirm your details",
|
||||
"default": {"name": "Bob"},
|
||||
},
|
||||
)
|
||||
assert result.structured_content is not None
|
||||
|
||||
|
||||
class TestBackfillBooleanDefaults:
|
||||
def test_missing_bool_with_default_gets_backfilled(self):
|
||||
data = {"title": "Note", "content": "Body"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue