Coverage for /Users/antonigmitruk/golf/src/golf/utilities/elicitation.py: 0%
47 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-08-16 18:46 +0200
« prev ^ index » next coverage.py v7.6.12, created at 2025-08-16 18:46 +0200
1"""Elicitation utilities for Golf MCP tools.
3This module provides simplified elicitation functions that Golf tool authors
4can use without needing to manage FastMCP Context objects directly.
5"""
7from typing import Any, TypeVar, overload
8from collections.abc import Callable
10from .context import get_current_context
12T = TypeVar("T")
14# Apply telemetry instrumentation if available
15try:
16 from golf.telemetry import instrument_elicitation
18 _instrumentation_available = True
19except ImportError:
20 _instrumentation_available = False
22 def instrument_elicitation(func: Callable, elicitation_type: str = "elicit") -> Callable:
23 """No-op instrumentation when telemetry is not available."""
24 return func
27@overload
28async def elicit(
29 message: str,
30 response_type: None = None,
31) -> dict[str, Any]:
32 """Elicit with no response type returns empty dict."""
33 ...
36@overload
37async def elicit(
38 message: str,
39 response_type: type[T],
40) -> T:
41 """Elicit with response type returns typed data."""
42 ...
45@overload
46async def elicit(
47 message: str,
48 response_type: list[str],
49) -> str:
50 """Elicit with list of options returns selected string."""
51 ...
54async 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.
60 This is a simplified wrapper around FastMCP's Context.elicit() method
61 that automatically handles context retrieval and response processing.
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
70 Returns:
71 The user's response in the requested format
73 Raises:
74 RuntimeError: If called outside MCP context or user declines/cancels
75 ValueError: If response validation fails
77 Examples:
78 ```python
79 from golf.utilities import elicit
80 from pydantic import BaseModel
82 class UserInfo(BaseModel):
83 name: str
84 email: str
86 async def collect_user_info():
87 # Structured elicitation
88 info = await elicit("Please provide your details:", UserInfo)
90 # Simple text elicitation
91 reason = await elicit("Why do you need this?", str)
93 # Multiple choice elicitation
94 priority = await elicit("Select priority:", ["low", "medium", "high"])
96 # Confirmation elicitation
97 await elicit("Proceed with the action?")
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()
106 # Call the context's elicit method
107 result = await ctx.elicit(message, response_type)
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
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
129async def elicit_confirmation(message: str) -> bool:
130 """Request a simple yes/no confirmation from the user.
132 This is a convenience function for common confirmation prompts.
134 Args:
135 message: The confirmation message to show the user
137 Returns:
138 True if user confirmed, False if declined
140 Raises:
141 RuntimeError: If user cancels or other error occurs
143 Example:
144 ```python
145 from golf.utilities import elicit_confirmation
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
168# Apply instrumentation to all elicitation functions
169elicit = instrument_elicitation(elicit, "elicit")
170elicit_confirmation = instrument_elicitation(elicit_confirmation, "confirmation")