|
10 | 10 | asynccontextmanager, |
11 | 11 | ) |
12 | 12 | from itertools import chain |
13 | | -from typing import Any, Generic, Literal |
| 13 | +from typing import Any, Generic, Literal, TypeVar |
14 | 14 |
|
15 | 15 | import anyio |
16 | 16 | import pydantic_core |
17 | | -from pydantic import BaseModel, Field |
| 17 | +from pydantic import BaseModel, Field, ValidationError |
18 | 18 | from pydantic.networks import AnyUrl |
19 | 19 | from pydantic_settings import BaseSettings, SettingsConfigDict |
20 | 20 | from starlette.applications import Starlette |
|
67 | 67 |
|
68 | 68 | logger = get_logger(__name__) |
69 | 69 |
|
| 70 | +ElicitedModelT = TypeVar("ElicitedModelT", bound=BaseModel) |
| 71 | + |
70 | 72 |
|
71 | 73 | class Settings(BaseSettings, Generic[LifespanResultT]): |
72 | 74 | """FastMCP server settings. |
@@ -1005,35 +1007,48 @@ async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContent |
1005 | 1007 | async def elicit( |
1006 | 1008 | self, |
1007 | 1009 | message: str, |
1008 | | - requestedSchema: dict[str, Any], |
1009 | | - ) -> dict[str, Any]: |
| 1010 | + schema: type[ElicitedModelT], |
| 1011 | + ) -> ElicitedModelT: |
1010 | 1012 | """Elicit information from the client/user. |
1011 | 1013 |
|
1012 | 1014 | This method can be used to interactively ask for additional information from the |
1013 | | - client within a tool's execution. |
1014 | | - The client might display the message to the user and collect a response |
1015 | | - according to the provided schema. Or in case a client is an agent, it might |
1016 | | - decide how to handle the elicitation -- either by asking the user or |
1017 | | - automatically generating a response. |
| 1015 | + client within a tool's execution. The client might display the message to the |
| 1016 | + user and collect a response according to the provided schema. Or in case a |
| 1017 | + client |
| 1018 | + is an agent, it might decide how to handle the elicitation -- either by asking |
| 1019 | + the user or automatically generating a response. |
1018 | 1020 |
|
1019 | 1021 | Args: |
1020 | | - message: The message to present to the user |
1021 | | - requestedSchema: JSON Schema defining the expected response structure |
| 1022 | + schema: A Pydantic model class defining the expected response structure |
| 1023 | + message: Optional message to present to the user. If not provided, will use |
| 1024 | + a default message based on the schema |
1022 | 1025 |
|
1023 | 1026 | Returns: |
1024 | | - The user's response as a dict matching the request schema structure |
| 1027 | + An instance of the schema type with the user's response |
1025 | 1028 |
|
1026 | 1029 | Raises: |
1027 | | - ValueError: If elicitation is not supported by the client or fails |
| 1030 | + Exception: If the user declines or cancels the elicitation |
| 1031 | + ValidationError: If the response doesn't match the schema |
1028 | 1032 | """ |
1029 | 1033 |
|
| 1034 | + json_schema = schema.model_json_schema() |
| 1035 | + |
1030 | 1036 | result = await self.request_context.session.elicit( |
1031 | 1037 | message=message, |
1032 | | - requestedSchema=requestedSchema, |
| 1038 | + requestedSchema=json_schema, |
1033 | 1039 | related_request_id=self.request_id, |
1034 | 1040 | ) |
1035 | 1041 |
|
1036 | | - return result.content |
| 1042 | + if result.action == "accept" and result.content: |
| 1043 | + # Validate and parse the content using the schema |
| 1044 | + try: |
| 1045 | + return schema.model_validate(result.content) |
| 1046 | + except ValidationError as e: |
| 1047 | + raise ValueError(f"Invalid response: {e}") |
| 1048 | + elif result.action == "decline": |
| 1049 | + raise Exception("User declined to provide information") |
| 1050 | + else: |
| 1051 | + raise Exception("User cancelled the request") |
1037 | 1052 |
|
1038 | 1053 | async def log( |
1039 | 1054 | self, |
|
0 commit comments