Coverage for /Users/antonigmitruk/golf/src/golf/utilities/elicitation.py: 0%

1"""Elicitation utilities for Golf MCP tools.

2

3 This module provides simplified elicitation functions that Golf tool authors

4 can use without needing to manage FastMCP Context objects directly.

5"""

6

7 from typing import Any, TypeVar, overload

8 from collections.abc import Callable

9

10 from .context import get_current_context

11

12 T = TypeVar("T")

13

14# Apply telemetry instrumentation if available

15 try:

16 from golf.telemetry import instrument_elicitation

17

18 _instrumentation_available = True

19 except ImportError:

20 _instrumentation_available = False

21

22 def instrument_elicitation(func: Callable, elicitation_type: str = "elicit") -> Callable:

23 """No-op instrumentation when telemetry is not available."""

24 return func

25

26

27@overload

28 async def elicit(

29 message: str,

30 response_type: None = None,

31) -> dict[str, Any]:

32 """Elicit with no response type returns empty dict."""

33 ...

34

35

36@overload

37 async def elicit(

38 message: str,

39 response_type: type[T],

40) -> T:

41 """Elicit with response type returns typed data."""

42 ...

43

44

45@overload

46 async def elicit(

47 message: str,

48 response_type: list[str],

49) -> str:

50 """Elicit with list of options returns selected string."""

51 ...

52

53

54 async def elicit(

55 message: str,

56 response_type: type[T] | list[str] | None = None,

57) -> T | dict[str, Any] | str:

58 """Request additional information from the user via MCP elicitation.

59

60 This is a simplified wrapper around FastMCP's Context.elicit() method

61 that automatically handles context retrieval and response processing.

62

63 Args:

64 message: Human-readable message explaining what information is needed

65 response_type: The type of response expected:

66 - None: Returns empty dict (for confirmation prompts)

67 - type[T]: Returns validated instance of T (BaseModel, dataclass, etc.)

68 - list[str]: Returns selected string from the options

69

70 Returns:

71 The user's response in the requested format

72

73 Raises:

74 RuntimeError: If called outside MCP context or user declines/cancels

75 ValueError: If response validation fails

76

77 Examples:

78 ```python

79 from golf.utilities import elicit

80 from pydantic import BaseModel

81

82 class UserInfo(BaseModel):

83 name: str

84 email: str

85

86 async def collect_user_info():

87 # Structured elicitation

88 info = await elicit("Please provide your details:", UserInfo)

89

90 # Simple text elicitation

91 reason = await elicit("Why do you need this?", str)

92

93 # Multiple choice elicitation

94 priority = await elicit("Select priority:", ["low", "medium", "high"])

95

96 # Confirmation elicitation

97 await elicit("Proceed with the action?")

98

99 return f"User {info.name} requested {reason} with {priority} priority"

100 ```

101 """

102 try:

103 # Get the current FastMCP context

104 ctx = get_current_context()

105

106 # Call the context's elicit method

107 result = await ctx.elicit(message, response_type)

108

109 # Handle the response based on the action

110 if hasattr(result, "action"):

111 if result.action == "accept":

112 return result.data

113 elif result.action == "decline":

114 raise RuntimeError(f"User declined the elicitation request: {message}")

115 elif result.action == "cancel":

116 raise RuntimeError(f"User cancelled the elicitation request: {message}")

117 else:

118 raise RuntimeError(f"Unexpected elicitation response: {result.action}")

119 else:

120 # Direct response (shouldn't happen with current FastMCP)

121 return result

122

123 except Exception as e:

124 if isinstance(e, RuntimeError):

125 raise # Re-raise our custom errors

126 raise RuntimeError(f"Elicitation failed: {str(e)}") from e

127

128

129 async def elicit_confirmation(message: str) -> bool:

130 """Request a simple yes/no confirmation from the user.

131

132 This is a convenience function for common confirmation prompts.

133

134 Args:

135 message: The confirmation message to show the user

136

137 Returns:

138 True if user confirmed, False if declined

139

140 Raises:

141 RuntimeError: If user cancels or other error occurs

142

143 Example:

144 ```python

145 from golf.utilities import elicit_confirmation

146

147 async def delete_file(filename: str):

148 confirmed = await elicit_confirmation(

149 f"Are you sure you want to delete {filename}?"

150 )

151 if confirmed:

152 # Proceed with deletion

153 return f"Deleted {filename}"

154 else:

155 return "Deletion cancelled"

156 ```

157 """

158 try:

159 # Use elicitation with boolean choice

160 choice = await elicit(message, ["yes", "no"])

161 return choice.lower() == "yes"

162 except RuntimeError as e:

163 if "declined" in str(e):

164 return False

165 raise # Re-raise cancellation or other errors

166

167

168# Apply instrumentation to all elicitation functions

169 elicit = instrument_elicitation(elicit, "elicit")

170 elicit_confirmation = instrument_elicitation(elicit_confirmation, "confirmation")